@yoltra/react 0.3.0 → 0.5.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.
- package/README.es.md +63 -4
- package/README.md +62 -4
- package/dist/index.cjs +19 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +442 -358
- package/dist/index.mjs.map +1 -1
- package/dist/types/context/StoreContext.d.ts +1 -1
- package/dist/types/createYoltra.d.ts +20 -3
- package/dist/types/entity/useEntity.d.ts +32 -0
- package/dist/types/hooks/createHooks.d.ts +7 -1
- package/dist/types/hooks/hooks.d.ts +14 -1
- package/dist/types/hooks/suspense.d.ts +66 -0
- package/dist/types/index.d.ts +12 -10
- package/dist/types/utils/declaredProjection.d.ts +40 -0
- package/package.json +25 -9
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EntityAdapter, EntityId } from '@yoltra/core';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribes to a collection's order.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This is the subscription a list container wants, and the only one that should wake when the
|
|
7
|
+
* collection is reordered. Rows use {@link useEntity} or {@link useEntityField} and stay
|
|
8
|
+
* asleep through a sort.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare function useEntityIds<T, Id extends EntityId, R extends string>(reducer: R, adapter: EntityAdapter<T, Id>): readonly Id[];
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to one entity.
|
|
15
|
+
*
|
|
16
|
+
* @returns The entity, or `undefined` once it has been removed — a row that outlives its data
|
|
17
|
+
* for one render is normal, and returning `undefined` is what lets it render nothing rather
|
|
18
|
+
* than throw.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export declare function useEntity<T, Id extends EntityId, R extends string>(reducer: R, adapter: EntityAdapter<T, Id>, id: Id): T | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribes to one field of one entity.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* The narrowest subscription available, and the reason the shape is worth adopting: editing a
|
|
28
|
+
* title wakes the components reading that title and nothing else.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export declare function useEntityField<T, Id extends EntityId, R extends string, K extends keyof T & string>(reducer: R, adapter: EntityAdapter<T, Id>, id: Id, field: K): T[K] | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DeepReadonly, Dotted, Emit, Event, EventMapBase, EventPhase, PathValue, StoreInstance, WithGlob } from '@yoltra/core';
|
|
2
|
+
import { UseSuspenseAtomicProp, UseSuspenseAtomicProps } from './suspense.js';
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
/**
|
|
4
5
|
* Call signature for the typed `useAtomicProp` hook returned by {@link createHooks}.
|
|
@@ -137,6 +138,10 @@ export interface YoltraHooks<R extends string, S extends Record<R, any>, EM exte
|
|
|
137
138
|
useAtomicProps: UseAtomicProps<R, S>;
|
|
138
139
|
/** Runs a handler for a specific `(channel, type)` event. */
|
|
139
140
|
useEvent: UseEvent<EM, S>;
|
|
141
|
+
/** Suspense-loading variant of `useAtomicProp`, bound to the same context. */
|
|
142
|
+
useSuspenseAtomicProp: UseSuspenseAtomicProp<R, S>;
|
|
143
|
+
/** Suspense-loading variant of `useAtomicProps`, bound to the same context. */
|
|
144
|
+
useSuspenseAtomicProps: UseSuspenseAtomicProps<R, S>;
|
|
140
145
|
/** Shallow object equality using `Object.is` per-key. */
|
|
141
146
|
shallowEqual: <T extends Record<string, unknown>>(a: T, b: T) => boolean;
|
|
142
147
|
}
|
|
@@ -153,7 +158,8 @@ export interface YoltraHooks<R extends string, S extends Record<R, any>, EM exte
|
|
|
153
158
|
*
|
|
154
159
|
* @param StoreContext - A React context carrying a `StoreInstance<R, S, EM>`.
|
|
155
160
|
* @returns An object with typed hooks: `useStore`, `useEmit`, `useSelector`,
|
|
156
|
-
* `useAtomicProp`, `useAtomicProps`, `useEvent`,
|
|
161
|
+
* `useAtomicProp`, `useAtomicProps`, `useEvent`, `useSuspenseAtomicProp`,
|
|
162
|
+
* `useSuspenseAtomicProps`, and `shallowEqual`.
|
|
157
163
|
*
|
|
158
164
|
* @throws If any returned hook is called outside a `<StoreProvider>`.
|
|
159
165
|
*
|
|
@@ -53,7 +53,7 @@ export declare function useStore<EM extends EventMapBase, R extends string, S ex
|
|
|
53
53
|
* @public
|
|
54
54
|
*/
|
|
55
55
|
export declare function useEmit<EM extends EventMapBase>(): Emit<EM>;
|
|
56
|
-
export { shallowEqual } from '../utils/shallowEqual';
|
|
56
|
+
export { shallowEqual } from '../utils/shallowEqual.js';
|
|
57
57
|
/**
|
|
58
58
|
* Selects a derived value from the store using an external-store subscription.
|
|
59
59
|
* Re-renders when the selected value changes per `isEqual`.
|
|
@@ -173,6 +173,19 @@ export declare function useAtomicProp<R extends string, S extends Record<R, any>
|
|
|
173
173
|
* );
|
|
174
174
|
* ```
|
|
175
175
|
*
|
|
176
|
+
* @remarks
|
|
177
|
+
* The selector receives **only the paths declared in `specs`**, not the whole store. Reading
|
|
178
|
+
* anything else yields `undefined` in production and throws in development, naming the path.
|
|
179
|
+
*
|
|
180
|
+
* That is deliberate, and it replaced a real bug: the two arguments used to be independent, so
|
|
181
|
+
* a component could subscribe to one path and read another. It compiled, it ran, and it worked
|
|
182
|
+
* for as long as the two happened to change together — this repository shipped exactly that in
|
|
183
|
+
* its own example, where a list subscribed to `todo.filter` while reading `todo.data` and
|
|
184
|
+
* re-rendered only because adding a todo also rewrote `filter.categories`.
|
|
185
|
+
*
|
|
186
|
+
* Correct code is unaffected. Code that read more than it declared was already wrong, and now
|
|
187
|
+
* says so on the first render rather than on the first day the coincidence breaks.
|
|
188
|
+
*
|
|
176
189
|
* @public
|
|
177
190
|
*/
|
|
178
191
|
export declare function useAtomicProps<R extends string, S extends Record<R, any>, T>(specs: Array<{
|
|
@@ -35,6 +35,19 @@ export interface SuspenseAtomicPropOptions<T, S> {
|
|
|
35
35
|
* many ms. Cached errors ignore this and are re-thrown until invalidated.
|
|
36
36
|
*/
|
|
37
37
|
staleTime?: number;
|
|
38
|
+
/**
|
|
39
|
+
* How long a **failed** load is remembered, in milliseconds.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* `0` or omitted — the default — delivers the error to the nearest boundary and then forgets
|
|
43
|
+
* it, so resetting that boundary retries the load. Held errors made a retry button unable to
|
|
44
|
+
* retry, which turned a transient failure into a permanent one.
|
|
45
|
+
*
|
|
46
|
+
* A positive value puts a floor between attempts, for a loader that fails fast and would
|
|
47
|
+
* otherwise be re-attempted on every reset. `null` holds the failure until something calls
|
|
48
|
+
* `invalidate`, which is the old behaviour and is now something you ask for.
|
|
49
|
+
*/
|
|
50
|
+
errorTtlMs?: number | null;
|
|
38
51
|
/** Optional extra key to differentiate cache entries for the same path. */
|
|
39
52
|
key?: string;
|
|
40
53
|
}
|
|
@@ -107,6 +120,19 @@ export interface SuspenseAtomicPropsOptions<T, S> {
|
|
|
107
120
|
* many ms. Cached errors ignore this and are re-thrown until invalidated.
|
|
108
121
|
*/
|
|
109
122
|
staleTime?: number;
|
|
123
|
+
/**
|
|
124
|
+
* How long a **failed** load is remembered, in milliseconds.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* `0` or omitted — the default — delivers the error to the nearest boundary and then forgets
|
|
128
|
+
* it, so resetting that boundary retries the load. Held errors made a retry button unable to
|
|
129
|
+
* retry, which turned a transient failure into a permanent one.
|
|
130
|
+
*
|
|
131
|
+
* A positive value puts a floor between attempts, for a loader that fails fast and would
|
|
132
|
+
* otherwise be re-attempted on every reset. `null` holds the failure until something calls
|
|
133
|
+
* `invalidate`, which is the old behaviour and is now something you ask for.
|
|
134
|
+
*/
|
|
135
|
+
errorTtlMs?: number | null;
|
|
110
136
|
/** Optional extra key to differentiate cache entries. */
|
|
111
137
|
key?: string;
|
|
112
138
|
}
|
|
@@ -198,3 +224,43 @@ export declare function invalidateAtomicPropsByReducer(reducer: string): void;
|
|
|
198
224
|
* @public
|
|
199
225
|
*/
|
|
200
226
|
export declare function clearSuspenseCache(): void;
|
|
227
|
+
/**
|
|
228
|
+
* Call signature for the typed `useSuspenseAtomicProp` returned by `createHooks`.
|
|
229
|
+
*
|
|
230
|
+
* Identical in behaviour to the package-level {@link useSuspenseAtomicProp}; the reducer union
|
|
231
|
+
* and state shape are fixed by the store the hooks were created for, so neither has to be
|
|
232
|
+
* supplied at the call site.
|
|
233
|
+
*
|
|
234
|
+
* @typeParam R - Reducer name union.
|
|
235
|
+
* @typeParam S - State record keyed by `R`.
|
|
236
|
+
*
|
|
237
|
+
* @public
|
|
238
|
+
*/
|
|
239
|
+
export type UseSuspenseAtomicProp<R extends string, S extends Record<R, any>> = {
|
|
240
|
+
<R1 extends R, P extends Dotted<S[R1]>, T>(storeSpec: {
|
|
241
|
+
reducer: R1;
|
|
242
|
+
property: P;
|
|
243
|
+
}, options: SuspenseAtomicPropOptions<T, S>): T;
|
|
244
|
+
<R1 extends R, T>(storeSpec: {
|
|
245
|
+
reducer: R1;
|
|
246
|
+
property: string;
|
|
247
|
+
}, options: SuspenseAtomicPropOptions<T, S>): T;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Call signature for the typed `useSuspenseAtomicProps` returned by `createHooks`.
|
|
251
|
+
*
|
|
252
|
+
* @typeParam R - Reducer name union.
|
|
253
|
+
* @typeParam S - State record keyed by `R`.
|
|
254
|
+
*
|
|
255
|
+
* @public
|
|
256
|
+
*/
|
|
257
|
+
export type UseSuspenseAtomicProps<R extends string, S extends Record<R, any>> = {
|
|
258
|
+
<R1 extends R, T>(specs: Array<{
|
|
259
|
+
reducer: R1;
|
|
260
|
+
property: WithGlob<Dotted<S[R1]>> | ReadonlyArray<WithGlob<Dotted<S[R1]>>>;
|
|
261
|
+
}>, options: SuspenseAtomicPropsOptions<T, S>): T;
|
|
262
|
+
<R1 extends R, T>(specs: Array<{
|
|
263
|
+
reducer: R1;
|
|
264
|
+
property: string | readonly string[];
|
|
265
|
+
}>, options: SuspenseAtomicPropsOptions<T, S>): T;
|
|
266
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @yoltra/react
|
|
3
3
|
*/
|
|
4
|
-
export { StoreContext } from './context/StoreContext';
|
|
5
|
-
export { StoreProvider } from './context/StoreProvider';
|
|
6
|
-
export { shallowEqual,
|
|
7
|
-
export { clearSuspenseCache, invalidateAtomicProp, invalidateAtomicPropsByReducer, suspenseCache,
|
|
8
|
-
export { createHooks } from './hooks/createHooks';
|
|
9
|
-
export type { UseAtomicProp, UseAtomicProps, UseEvent, YoltraHooks } from './hooks/createHooks';
|
|
10
|
-
export {
|
|
11
|
-
export
|
|
12
|
-
export type {
|
|
13
|
-
export type {
|
|
4
|
+
export { StoreContext } from './context/StoreContext.js';
|
|
5
|
+
export { StoreProvider } from './context/StoreProvider.js';
|
|
6
|
+
export { shallowEqual, useEmit, useEvent, useSelector, useStore } from './hooks/hooks.js';
|
|
7
|
+
export { clearSuspenseCache, invalidateAtomicProp, invalidateAtomicPropsByReducer, suspenseCache, } from './hooks/suspense.js';
|
|
8
|
+
export { createHooks } from './hooks/createHooks.js';
|
|
9
|
+
export type { UseAtomicProp, UseAtomicProps, UseEvent, YoltraHooks } from './hooks/createHooks.js';
|
|
10
|
+
export type { UseSuspenseAtomicProp, UseSuspenseAtomicProps } from './hooks/suspense.js';
|
|
11
|
+
export { createYoltra } from './createYoltra.js';
|
|
12
|
+
export type { Yoltra } from './createYoltra.js';
|
|
13
|
+
export type { OneOrMany, PathValue } from './hooks/hooks.js';
|
|
14
|
+
export type { SuspenseAtomicPropOptions, SuspenseAtomicPropsOptions } from './hooks/suspense.js';
|
|
15
|
+
export { useEntity, useEntityField, useEntityIds } from './entity/useEntity.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building the slice of state a component actually declared.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* `useAtomicProps` used to take two independent arguments: a list of paths that wake the
|
|
6
|
+
* component, and a selector handed the *entire* state. Nothing tied them together, so
|
|
7
|
+
* declaring one path and reading another compiled, ran, and worked — until the day the two
|
|
8
|
+
* stopped changing together. This repository shipped that bug in its own example: a list
|
|
9
|
+
* subscribed to `todo.filter` while reading `todo.data`, and re-rendered only because adding a
|
|
10
|
+
* todo happened to rewrite `filter.categories` too.
|
|
11
|
+
*
|
|
12
|
+
* The fix is not to detect the mismatch but to remove the opportunity. The selector is handed
|
|
13
|
+
* a projection containing the declared paths and nothing else, so reading undeclared state is
|
|
14
|
+
* no longer a mistake that can be made silently — it reads `undefined` immediately, on the
|
|
15
|
+
* first render, instead of a correct value that goes stale later.
|
|
16
|
+
*
|
|
17
|
+
* In development it does better than `undefined`: the projection is wrapped in a guard that
|
|
18
|
+
* throws and names the path, because a `TypeError` three frames downstream is a poor way to
|
|
19
|
+
* learn you forgot a subscription.
|
|
20
|
+
*
|
|
21
|
+
* @module @yoltra/react
|
|
22
|
+
*/
|
|
23
|
+
/** A declared subscription, already normalized. */
|
|
24
|
+
export interface DeclaredPath {
|
|
25
|
+
readonly reducer: string;
|
|
26
|
+
/** Dotted path within the reducer's slice. `""` means the whole slice. */
|
|
27
|
+
readonly property: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds the projection of `state` that `declared` covers.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Leaves are copied by reference, so nothing is cloned and identity-based memoization
|
|
34
|
+
* downstream keeps working. Only the containers needed to reach a declared leaf are rebuilt.
|
|
35
|
+
*
|
|
36
|
+
* A declared path that does not exist in state is still written, as `undefined`. That keeps
|
|
37
|
+
* "declared but absent" distinguishable from "never declared", which is what lets the
|
|
38
|
+
* development guard tell a missing subscription from a missing value.
|
|
39
|
+
*/
|
|
40
|
+
export declare function projectDeclared(state: unknown, declared: readonly DeclaredPath[]): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoltra/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "React bindings for Yoltra",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -40,9 +40,8 @@
|
|
|
40
40
|
"node": ">=18.18"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@yoltra/core": "^0.
|
|
44
|
-
"react": "^18 || ^19"
|
|
45
|
-
"react-dom": "^18 || ^19"
|
|
43
|
+
"@yoltra/core": "^0.5.0",
|
|
44
|
+
"react": "^18 || ^19"
|
|
46
45
|
},
|
|
47
46
|
"peerDependenciesMeta": {
|
|
48
47
|
"@yoltra/core": {
|
|
@@ -84,7 +83,7 @@
|
|
|
84
83
|
"vite-tsconfig-paths": "^4.3.2",
|
|
85
84
|
"vite": "^7.1.11",
|
|
86
85
|
"vitest": "3.2.4",
|
|
87
|
-
"@yoltra/core": "0.
|
|
86
|
+
"@yoltra/core": "0.5.0"
|
|
88
87
|
},
|
|
89
88
|
"exports": {
|
|
90
89
|
".": {
|
|
@@ -96,14 +95,31 @@
|
|
|
96
95
|
},
|
|
97
96
|
"./package.json": "./package.json"
|
|
98
97
|
},
|
|
98
|
+
"sizeLimit": [
|
|
99
|
+
{
|
|
100
|
+
"name": "barrel",
|
|
101
|
+
"limitKb": 6.5,
|
|
102
|
+
"external": [
|
|
103
|
+
"react",
|
|
104
|
+
"@yoltra/core",
|
|
105
|
+
"tslib"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
],
|
|
99
109
|
"scripts": {
|
|
100
|
-
"build": "tsc -p tsconfig.build.json && vite build",
|
|
110
|
+
"build": "tsc -p tsconfig.build.json && vite build && node ../../tools/repo-tools/bin/dts-extensions.mjs dist/types",
|
|
101
111
|
"lint": "node ../../tools/repo-tools/bin/repo-eslint.cjs --report-unused-disable-directives --max-warnings 0",
|
|
112
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.vitest.json --noEmit",
|
|
102
113
|
"lint:fix": "node ../../tools/repo-tools/bin/repo-eslint.cjs --fix",
|
|
103
114
|
"test": "vitest --coverage --watch=false",
|
|
104
115
|
"test:watch": "vitest --coverage",
|
|
105
|
-
"docs": "rushx docs:js && rushx docs:md",
|
|
106
|
-
"docs:
|
|
107
|
-
"docs:
|
|
116
|
+
"docs": "rushx docs:js && rushx docs:md && rushx docs:stamp",
|
|
117
|
+
"docs:stamp": "node ../../tools/repo-tools/bin/docs-stamp.mjs docs",
|
|
118
|
+
"docs:md": "typedoc --options ./typedoc.react.json",
|
|
119
|
+
"docs:js": "typedoc --options ./typedoc.react.json --json ./.typedoc/react-en.json",
|
|
120
|
+
"size": "node ../../tools/repo-tools/bin/size-check.cjs",
|
|
121
|
+
"bench": "vitest bench --run",
|
|
122
|
+
"bench:check": "node ../../tools/repo-tools/bin/bench-check.cjs",
|
|
123
|
+
"bench:record": "node ../../tools/repo-tools/bin/bench-check.cjs --record"
|
|
108
124
|
}
|
|
109
125
|
}
|