@yoltra/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ import { DeepReadonly, Dotted, Emit, Event, EventMapBase, EventPhase, PathValue, StoreInstance, WithGlob } from '@yoltra/core';
2
+ /**
3
+ * Re-export of {@link PathValue} from `@yoltra/core`.
4
+ *
5
+ * Resolves the TypeScript type at a dotted path `P` within object type `T`.
6
+ * See the core definition for full documentation.
7
+ *
8
+ * @public
9
+ */
10
+ export type { PathValue };
11
+ /**
12
+ * Accepts either a single value or a readonly array of that value.
13
+ * Useful for APIs that take one-or-many keys.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * function takeIds(ids: OneOrMany<string>) { /* ... *\/ }
18
+ * takeIds('a');
19
+ * takeIds(['a','b'] as const);
20
+ * ```
21
+ *
22
+ * @public
23
+ */
24
+ export type OneOrMany<T> = T | readonly T[];
25
+ /**
26
+ * Returns the current {@link StoreInstance} from {@link StoreContext}.
27
+ * Throws if used outside of a `<StoreProvider>`.
28
+ *
29
+ * @typeParam EM - Event map type.
30
+ * @typeParam R - Reducer name union.
31
+ * @typeParam S - State record keyed by `R`.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const store = useStore<MyEM, 'counter' | 'todos', AppState>();
36
+ * const state = store.getState();
37
+ * ```
38
+ *
39
+ * @public
40
+ */
41
+ export declare function useStore<EM extends EventMapBase, R extends string, S extends Record<R, any>>(): StoreInstance<R, S, EM>;
42
+ /**
43
+ * Returns the store's `emit` function (stable reference).
44
+ *
45
+ * @typeParam EM - Event map type.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const emit = useEmit<MyEM>();
50
+ * await emit('ui', 'toggle', true);
51
+ * ```
52
+ *
53
+ * @public
54
+ */
55
+ export declare function useEmit<EM extends EventMapBase>(): Emit<EM>;
56
+ /**
57
+ * Shallow object equality using `Object.is` per-key.
58
+ *
59
+ * Useful as the `isEqual` argument for `useAtomicProp` and `useAtomicProps`
60
+ * when the derived value is a plain object. Also available from the object
61
+ * returned by {@link createHooks}.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * shallowEqual({ a: 1 }, { a: 1 }); // true
66
+ * shallowEqual({ a: 1 }, { a: 2 }); // false
67
+ * ```
68
+ *
69
+ * @public
70
+ */
71
+ export declare function shallowEqual<T extends Record<string, any>>(a: T, b: T): boolean;
72
+ /**
73
+ * Selects a derived value from the store using an external-store subscription.
74
+ * Re-renders when the selected value changes per `isEqual`.
75
+ *
76
+ * @typeParam S - State type returned by `getState()`.
77
+ * @typeParam T - Selected value type.
78
+ * @param selector - `(state) => value` derived from the current state.
79
+ * @param isEqual - Optional equality comparator (defaults to `Object.is`).
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * const total = useSelector((s: AppState) => s.todos.items.length);
84
+ * ```
85
+ *
86
+ * @public
87
+ */
88
+ export declare function useSelector<S extends Record<any, any>, T>(selector: (state: DeepReadonly<S>) => T, isEqual?: (a: T, b: T) => boolean): T;
89
+ /**
90
+ * Fine-grained **single-path** selector for a reducer's state.
91
+ *
92
+ * Re-renders only when the specified `reducer.property` (dotted path) actually changes.
93
+ * For most applications, prefer using the typed version from {@link createHooks}
94
+ * which infers all type parameters automatically.
95
+ *
96
+ * **Supports**
97
+ * - Exact root prop: `{ reducer: "todo", property: "data" }`
98
+ * - Exact deep path: `{ reducer: "todo", property: "data.123.title" }`
99
+ * - Wildcards (pattern): `{ reducer: "todo", property: "data.*" }` or `"data.**"`
100
+ *
101
+ * **Overloads**
102
+ * - Exact path (no `*`): returns the precise `PathValue` when `map` is omitted
103
+ * - Exact path + `map`: returns `T` from `map(value)`
104
+ * - Glob path (with `*`/`**`): requires `map` and returns `T` from `map(state)`
105
+ *
106
+ * @example Via createHooks (recommended)
107
+ * ```tsx
108
+ * const { useAtomicProp } = createHooks(AppStoreContext);
109
+ *
110
+ * function TodoTitle({ index }: { index: number }) {
111
+ * // Types are inferred — no explicit generics needed
112
+ * const title = useAtomicProp({
113
+ * reducer: 'todos',
114
+ * property: `items.${index}.title`,
115
+ * });
116
+ * return <span>{title}</span>;
117
+ * }
118
+ * ```
119
+ *
120
+ * @example Standalone with explicit generics
121
+ * ```tsx
122
+ * const title = useAtomicProp<'todos', AppState, 'todos', 'items.0.title'>(
123
+ * { reducer: 'todos', property: 'items.0.title' }
124
+ * );
125
+ * ```
126
+ *
127
+ * @example Map over exact path
128
+ * ```tsx
129
+ * const len = useAtomicProp(
130
+ * { reducer: 'todos', property: 'items' },
131
+ * items => items.length
132
+ * );
133
+ * ```
134
+ *
135
+ * @example Glob pattern over state
136
+ * ```tsx
137
+ * const titles = useAtomicProp(
138
+ * { reducer: 'todos', property: 'items.**' },
139
+ * state => state.items.map(x => x.title),
140
+ * shallowEqual
141
+ * );
142
+ * ```
143
+ *
144
+ * @public
145
+ */
146
+ export declare function useAtomicProp<R extends string, S extends Record<R, any>, R1 extends R, P extends Dotted<S[R1]>>(spec: {
147
+ reducer: R1;
148
+ property: P;
149
+ }): PathValue<S[R1], P>;
150
+ export declare function useAtomicProp<R extends string, S extends Record<R, any>, R1 extends R, P extends Dotted<S[R1]>, T>(spec: {
151
+ reducer: R1;
152
+ property: P;
153
+ }, map: (value: PathValue<S[R1], P>) => T, isEqual?: (a: T, b: T) => boolean): T;
154
+ export declare function useAtomicProp<R extends string, S extends Record<R, any>, R1 extends R, P extends WithGlob<Dotted<S[R1]>>, T>(spec: {
155
+ reducer: R1;
156
+ property: P;
157
+ }, map: (value: any) => T, isEqual?: (a: T, b: T) => boolean): T;
158
+ export declare function useAtomicProp<R extends string, S extends Record<R, any>>(spec: {
159
+ reducer: R;
160
+ property: string;
161
+ }): unknown;
162
+ export declare function useAtomicProp<R extends string, S extends Record<R, any>, T>(spec: {
163
+ reducer: R;
164
+ property: string;
165
+ }, map: (value: any) => T, isEqual?: (a: T, b: T) => boolean): T;
166
+ /**
167
+ * **Multi-path** fine-grained selector.
168
+ *
169
+ * Subscribes to several `reducer.property` paths (supports deep & wildcard)
170
+ * and recomputes `selector(state)` when any of them change.
171
+ *
172
+ * @typeParam R - Slice name union.
173
+ * @typeParam S - State record keyed by `R`.
174
+ * @typeParam T - Derived value type.
175
+ *
176
+ * @param specs - Array of `{ reducer, property }`, where `property` can be a string or array of strings. Supports `*`/`**`.
177
+ * @param selector - `(state) => T` function run against the full state.
178
+ * @param isEqual - Equality comparator for the derived value (defaults to `Object.is`).
179
+ *
180
+ * @example
181
+ * ```tsx
182
+ * const total = useAtomicProps<'todos' | 'filter', AppState, number>(
183
+ * [
184
+ * { reducer: 'todos', property: 'items.**' },
185
+ * { reducer: 'filter', property: 'q' }
186
+ * ],
187
+ * (s) => s.todos.items.filter(x => x.title.includes(s.filter.q)).length
188
+ * );
189
+ * ```
190
+ *
191
+ * @public
192
+ */
193
+ export declare function useAtomicProps<R extends string, S extends Record<R, any>, T>(specs: Array<{
194
+ reducer: R;
195
+ property: OneOrMany<WithGlob<Dotted<S[R]>>>;
196
+ }>, selector: (state: DeepReadonly<S>) => T, isEqual?: (a: T, b: T) => boolean): T;
197
+ export declare function useAtomicProps<R extends string, S extends Record<R, any>, T>(specs: Array<{
198
+ reducer: R;
199
+ property: OneOrMany<string>;
200
+ }>, selector: (state: DeepReadonly<S>) => T, isEqual?: (a: T, b: T) => boolean): T;
201
+ /**
202
+ * Subscribe to store events from a React component.
203
+ *
204
+ * This hook enables reactive UI patterns by allowing components to respond
205
+ * to specific events without selecting state. Useful for:
206
+ * - Showing notifications on certain events
207
+ * - Triggering animations
208
+ * - Logging/analytics
209
+ * - Responding to rejected (uncommitted) events
210
+ *
211
+ * **Phases:**
212
+ * - `'committed'` (default): Events that passed middleware and reached reducers
213
+ * - `'uncommitted'`: Events rejected by middleware
214
+ * - `'all'`: Both committed and uncommitted events (handler receives phase parameter)
215
+ *
216
+ * @typeParam EM - Event map type.
217
+ * @typeParam C - Channel key within `EM`.
218
+ * @typeParam T - Event type key within channel `C`.
219
+ *
220
+ * @param channel - Event channel to subscribe to.
221
+ * @param type - Event type to subscribe to.
222
+ * @param handler - Handler called when the event fires. Receives `(event, getState, emit, phase)`.
223
+ * @param phase - Event phase to subscribe to (default: `'committed'`).
224
+ *
225
+ * @example Committed events (default)
226
+ * ```tsx
227
+ * useEvent('ui', 'save', (event, getState, emit, phase) => {
228
+ * showToast('Saved successfully!');
229
+ * });
230
+ * ```
231
+ *
232
+ * @example Rejected events
233
+ * ```tsx
234
+ * useEvent('ui', 'delete', (event, getState, emit, phase) => {
235
+ * showToast('Delete was blocked by middleware');
236
+ * }, 'uncommitted');
237
+ * ```
238
+ *
239
+ * @example All events
240
+ * ```tsx
241
+ * useEvent('ui', 'action', (event, getState, emit, phase) => {
242
+ * console.log('Action:', phase); // 'committed' or 'uncommitted'
243
+ * }, 'all');
244
+ * ```
245
+ *
246
+ * @public
247
+ */
248
+ export declare function useEvent<EM extends EventMapBase, C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (event: Event<EM, C, T>, getState: () => DeepReadonly<any>, emit: Emit<EM>, phase: "committed" | "uncommitted") => void | Promise<void>, phase?: EventPhase): void;
@@ -0,0 +1,178 @@
1
+ import { Dotted, WithGlob } from '@yoltra/core';
2
+ /**
3
+ * Default Suspense cache instance shared by all `useSuspense*` hooks.
4
+ *
5
+ * Use {@link invalidateAtomicProp}, {@link invalidateAtomicPropsByReducer},
6
+ * or {@link clearSuspenseCache} to manage the cache from outside hooks.
7
+ *
8
+ * @public
9
+ */
10
+ export declare const suspenseCache: SuspenseCache;
11
+ /**
12
+ * Options for {@link useSuspenseAtomicProp}.
13
+ *
14
+ * @typeParam T - The resolved value type after loading.
15
+ * @typeParam S - Store state record.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const options: SuspenseAtomicPropOptions<User, AppState> = {
20
+ * load: async (userId) => fetchUser(userId),
21
+ * staleTime: 30_000, // cache for 30 seconds
22
+ * key: 'user-detail',
23
+ * };
24
+ * ```
25
+ *
26
+ * @public
27
+ */
28
+ export interface SuspenseAtomicPropOptions<T, S> {
29
+ /** Async loader that receives the value at the path and the full slice. */
30
+ load: (valueAtPath: any, slice: S[keyof S]) => Promise<T> | T;
31
+ /** Time in ms before the cached value is considered stale (default: 0). */
32
+ staleTime?: number;
33
+ /** Optional extra key to differentiate cache entries for the same path. */
34
+ key?: string;
35
+ }
36
+ /**
37
+ * Suspense-compatible version of `useAtomicProp` that throws a promise while loading.
38
+ *
39
+ * Subscribes to a single dotted path and calls `options.load` to produce the
40
+ * resolved value. While the promise is pending, React Suspense catches it and
41
+ * renders the nearest `<Suspense>` fallback.
42
+ *
43
+ * @typeParam R - Reducer name union.
44
+ * @typeParam S - State record keyed by `R`.
45
+ * @typeParam P - Dotted path within `S[R]`.
46
+ * @typeParam T - Resolved value type.
47
+ *
48
+ * @param storeSpec - `{ reducer, property }` identifying the path to subscribe to.
49
+ * @param options - Loading options (see {@link SuspenseAtomicPropOptions}).
50
+ * @returns The resolved value of type `T`.
51
+ *
52
+ * @throws A `Promise` while loading (caught by React Suspense).
53
+ * @throws If called outside a `<StoreProvider>`.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * function UserName({ userId }: { userId: string }) {
58
+ * const name = useSuspenseAtomicProp(
59
+ * { reducer: 'users', property: `byId.${userId}.name` },
60
+ * { load: async (name) => name ?? (await fetchUser(userId)).name },
61
+ * );
62
+ * return <span>{name}</span>;
63
+ * }
64
+ *
65
+ * // Wrap with Suspense
66
+ * <Suspense fallback={<Spinner />}>
67
+ * <UserName userId="123" />
68
+ * </Suspense>
69
+ * ```
70
+ *
71
+ * @public
72
+ */
73
+ export declare function useSuspenseAtomicProp<R extends string, S extends Record<R, any>, P extends Dotted<S[R]>, T>(storeSpec: {
74
+ reducer: R;
75
+ property: P;
76
+ }, options: SuspenseAtomicPropOptions<T, S>): T;
77
+ export declare function useSuspenseAtomicProp<R extends string, S extends Record<R, any>, T>(storeSpec: {
78
+ reducer: R;
79
+ property: string;
80
+ }, options: SuspenseAtomicPropOptions<T, S>): T;
81
+ /**
82
+ * Options for {@link useSuspenseAtomicProps}.
83
+ *
84
+ * @typeParam T - The resolved value type after loading.
85
+ * @typeParam S - Store state record.
86
+ *
87
+ * @public
88
+ */
89
+ export interface SuspenseAtomicPropsOptions<T, S> {
90
+ /** Async loader that receives the full store state. */
91
+ load: (state: S) => Promise<T> | T;
92
+ /** Time in ms before the cached value is considered stale (default: 0). */
93
+ staleTime?: number;
94
+ /** Optional extra key to differentiate cache entries. */
95
+ key?: string;
96
+ }
97
+ /**
98
+ * Suspense-compatible version of `useAtomicProps` that throws a promise while loading.
99
+ *
100
+ * Subscribes to multiple dotted paths and calls `options.load` with the full state
101
+ * to produce the resolved value. While the promise is pending, React Suspense
102
+ * renders the nearest `<Suspense>` fallback.
103
+ *
104
+ * @typeParam R - Reducer name union.
105
+ * @typeParam S - State record keyed by `R`.
106
+ * @typeParam T - Resolved value type.
107
+ *
108
+ * @param specs - Array of `{ reducer, property }` paths to subscribe to.
109
+ * @param options - Loading options (see {@link SuspenseAtomicPropsOptions}).
110
+ * @returns The resolved value of type `T`.
111
+ *
112
+ * @throws A `Promise` while loading (caught by React Suspense).
113
+ * @throws If called outside a `<StoreProvider>`.
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * function Dashboard() {
118
+ * const stats = useSuspenseAtomicProps(
119
+ * [
120
+ * { reducer: 'orders', property: 'items.**' },
121
+ * { reducer: 'users', property: 'active' },
122
+ * ],
123
+ * { load: async (state) => computeDashboardStats(state) },
124
+ * );
125
+ * return <StatsGrid data={stats} />;
126
+ * }
127
+ * ```
128
+ *
129
+ * @public
130
+ */
131
+ export declare function useSuspenseAtomicProps<R extends string, S extends Record<R, any>, T>(specs: Array<{
132
+ reducer: R;
133
+ property: Dotted<S[R]> | WithGlob<Dotted<S[R]>> | ReadonlyArray<WithGlob<Dotted<S[R]>>>;
134
+ }>, options: SuspenseAtomicPropsOptions<T, S>): T;
135
+ export declare function useSuspenseAtomicProps<R extends string, S extends Record<R, any>, T>(specs: Array<{
136
+ reducer: R;
137
+ property: string | readonly string[];
138
+ }>, options: SuspenseAtomicPropsOptions<T, S>): T;
139
+ /**
140
+ * Invalidates the Suspense cache entry for a specific `reducer.property` path.
141
+ *
142
+ * @param reducer - Reducer (slice) name.
143
+ * @param property - Dotted property path.
144
+ * @param extraKey - Optional extra key if the hook was created with `options.key`.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * invalidateAtomicProp('users', 'byId.123.name');
149
+ * ```
150
+ *
151
+ * @public
152
+ */
153
+ export declare function invalidateAtomicProp(reducer: string, property: string, extraKey?: string): void;
154
+ /**
155
+ * Invalidates all Suspense cache entries for a given reducer (slice).
156
+ *
157
+ * @param reducer - Reducer (slice) name whose cache entries should be cleared.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * invalidateAtomicPropsByReducer('users');
162
+ * ```
163
+ *
164
+ * @public
165
+ */
166
+ export declare function invalidateAtomicPropsByReducer(reducer: string): void;
167
+ /**
168
+ * Clears the entire Suspense cache, forcing all `useSuspense*` hooks to re-load.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * // After a logout, clear all cached data
173
+ * clearSuspenseCache();
174
+ * ```
175
+ *
176
+ * @public
177
+ */
178
+ export declare function clearSuspenseCache(): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module @yoltra/react
3
+ */
4
+ export { StoreContext } from './context/StoreContext';
5
+ export { StoreProvider } from './context/StoreProvider';
6
+ export { shallowEqual, useAtomicProp, useAtomicProps, useEmit, useEvent, useSelector, useStore, } from './hooks/hooks';
7
+ export { clearSuspenseCache, invalidateAtomicProp, invalidateAtomicPropsByReducer, suspenseCache, useSuspenseAtomicProp, useSuspenseAtomicProps, } from './hooks/suspense';
8
+ export { createHooks } from './hooks/createHooks';
9
+ export type { UseAtomicProp, UseAtomicProps, UseEvent } from './hooks/createHooks';
10
+ export type { OneOrMany, PathValue } from './hooks/hooks';
11
+ export type { SuspenseAtomicPropOptions, SuspenseAtomicPropsOptions } from './hooks/suspense';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @module @yoltra/react
3
+ */
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@yoltra/react",
3
+ "version": "0.1.0",
4
+ "description": "React bindings for Yoltra",
5
+ "scripts": {
6
+ "build": "tsc -p tsconfig.build.json && vite build",
7
+ "lint": "node ../../tools/repo-tools/bin/repo-eslint.cjs --report-unused-disable-directives --max-warnings 0",
8
+ "lint:fix": "node ../../tools/repo-tools/bin/repo-eslint.cjs --fix",
9
+ "test": "pnpm vitest --coverage --watch=false",
10
+ "test:watch": "vitest --coverage",
11
+ "prepublishOnly": "node ../../common/scripts/copy-license.cjs",
12
+ "docs": "rushx docs:js && rushx docs:md",
13
+ "docs:md": "pnpm typedoc --options ./typedoc.react.json",
14
+ "docs:js": "pnpm typedoc --options ./typedoc.react.json --json ./.typedoc/react-en.json"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Manu Ramirez <@pixerael>",
19
+ "email": "manu@yoltra.dev"
20
+ },
21
+ "maintainers": [],
22
+ "homepage": "https://yoltra.dev",
23
+ "keywords": [
24
+ "state",
25
+ "store",
26
+ "react",
27
+ "redux-alternative",
28
+ "yoltra"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/yoltra/yoltra.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/yoltra/yoltra/issues"
36
+ },
37
+ "main": "dist/index.cjs",
38
+ "module": "dist/index.mjs",
39
+ "types": "dist/types/index.d.ts",
40
+ "private": false,
41
+ "sideEffects": false,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "files": [
46
+ "dist/",
47
+ "LICENSE",
48
+ "README.md"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18.18"
52
+ },
53
+ "peerDependencies": {
54
+ "@yoltra/core": "^0.1.0",
55
+ "react": "^18 || ^19",
56
+ "react-dom": "^18 || ^19"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@yoltra/core": {
60
+ "optional": false
61
+ }
62
+ },
63
+ "dependencies": {
64
+ "tslib": "^2.8.1"
65
+ },
66
+ "devDependencies": {
67
+ "@yoltra/core": "workspace:^0.1.0",
68
+ "@eslint/js": "^9.30.1",
69
+ "@rollup/plugin-commonjs": "^25.0.7",
70
+ "@rollup/plugin-node-resolve": "^15.2.3",
71
+ "@rollup/plugin-typescript": "^11.1.2",
72
+ "@testing-library/dom": "10.4.1",
73
+ "@testing-library/jest-dom": "^6.8.0",
74
+ "@testing-library/react": "16.3.0",
75
+ "@types/react-dom": "^19.1.6",
76
+ "@types/react": "^19.1.8",
77
+ "@types/node": "^24.0.12",
78
+ "@vitejs/plugin-react-swc": "^3.10.2",
79
+ "@vitejs/plugin-react": "^5.0.0",
80
+ "@vitest/coverage-v8": "3.2.4",
81
+ "eslint-plugin-react-hooks": "^5.2.0",
82
+ "eslint-plugin-react-refresh": "^0.4.20",
83
+ "vite-plugin-banner": "0.8.1",
84
+ "eslint": "^9.30.1",
85
+ "globals": "^16.3.0",
86
+ "jsdom": "^24.0.0",
87
+ "react-dom": "19.1.1",
88
+ "react": "19.1.1",
89
+ "rollup": "^4.16.0",
90
+ "typescript-eslint": "^8.35.1",
91
+ "typescript": "5.9.3",
92
+ "typedoc": "^0.28.13",
93
+ "typedoc-plugin-markdown": "4.9.0",
94
+ "typedoc-plugin-localization": "3.0.6",
95
+ "vite-plugin-dts": "^4.5.4",
96
+ "vite-tsconfig-paths": "^4.3.2",
97
+ "vite": "^7.1.11",
98
+ "vitest": "3.2.4"
99
+ },
100
+ "exports": {
101
+ ".": {
102
+ "types": "./dist/types/index.d.ts",
103
+ "react-native": "./dist/index.mjs",
104
+ "import": "./dist/index.mjs",
105
+ "require": "./dist/index.cjs",
106
+ "default": "./dist/index.mjs"
107
+ },
108
+ "./package.json": "./package.json"
109
+ }
110
+ }