@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.
@@ -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`, and `shallowEqual`.
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
+ };
@@ -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, 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, YoltraHooks } from './hooks/createHooks';
10
- export { createYoltra } from './createYoltra';
11
- export type { Yoltra } from './createYoltra';
12
- export type { OneOrMany, PathValue } from './hooks/hooks';
13
- export type { SuspenseAtomicPropOptions, SuspenseAtomicPropsOptions } from './hooks/suspense';
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.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.3.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.3.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:md": "pnpm typedoc --options ./typedoc.react.json",
107
- "docs:js": "pnpm typedoc --options ./typedoc.react.json --json ./.typedoc/react-en.json"
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
  }