@molecule/app-react 3.2.0 → 3.3.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.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-11-03T18:02:49.265Z
6
+ Generated: 2026-11-03T23:48:33.627Z
7
7
  -->
8
8
 
9
9
  # @molecule/app-react
@@ -1029,6 +1029,28 @@ interface UseOAuthReturn {
1029
1029
  }
1030
1030
  ```
1031
1031
 
1032
+ #### `UseOptimisticValueResult`
1033
+
1034
+ What {@link useOptimisticValue} returns: the state to render and the two ways to change it.
1035
+
1036
+ ```typescript
1037
+ interface UseOptimisticValueResult<T> extends OptimisticValueState<T> {
1038
+ /**
1039
+ * Shows `next` at once and saves it, after any save already running.
1040
+ *
1041
+ * @param next - The value the person chose.
1042
+ */
1043
+ set: (next: T) => void
1044
+
1045
+ /**
1046
+ * Like `set`, computed from the value shown now (`update((on) => !on)`).
1047
+ *
1048
+ * @param change - Computes the next value from the value shown now.
1049
+ */
1050
+ update: (change: (current: T) => T) => void
1051
+ }
1052
+ ```
1053
+
1032
1054
  #### `UsePasswordResetReturn`
1033
1055
 
1034
1056
  Return type for usePasswordReset hook.
@@ -2505,6 +2527,27 @@ function useOAuth(config?: {
2505
2527
 
2506
2528
  **Returns:** OAuth helpers: providers, getOAuthUrl, redirect, and loginViaPopup.
2507
2529
 
2530
+ #### `useOptimisticValue(options)`
2531
+
2532
+ A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
2533
+ setting switch — that changes on the first render after a tap and is saved one change at a
2534
+ time, in order. A tap during a save is saved right after it; only a failure of the newest
2535
+ change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
2536
+
2537
+ `value` is the server's value from props or a query. A new `value` is taken only when it
2538
+ differs (by `equals`) from the one the hook last received, and only while nothing is being
2539
+ saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
2540
+ latest `commit` and `onError` are always used. Give the component a `key` per record, so one
2541
+ record's pending change never shows on another.
2542
+
2543
+ ```typescript
2544
+ function useOptimisticValue<T>(options: OptimisticValueOptions<T>): UseOptimisticValueResult<T>
2545
+ ```
2546
+
2547
+ - `options` — The server's current value and how to save a change.
2548
+
2549
+ **Returns:** The value to render, whether a save is pending, the newest failure, and `set`/`update`.
2550
+
2508
2551
  #### `usePasswordReset()`
2509
2552
 
2510
2553
  Hook for password reset flow with async state tracking.
@@ -3383,7 +3426,7 @@ Peer dependencies:
3383
3426
  - `@molecule/app-i18n` ^1.0.1
3384
3427
  - `@molecule/app-logger` ^1.0.1
3385
3428
  - `@molecule/app-routing` ^1.0.1
3386
- - `@molecule/app-state` ^1.0.1
3429
+ - `@molecule/app-state` ^1.1.0
3387
3430
  - `@molecule/app-storage` ^1.0.1
3388
3431
  - `@molecule/app-theme` ^1.0.1
3389
3432
  - `@molecule/app-ui` ^1.0.1
@@ -16,6 +16,7 @@ export * from './useHttp.js';
16
16
  export * from './useLogger.js';
17
17
  export * from './useLogin.js';
18
18
  export * from './useOAuth.js';
19
+ export * from './useOptimisticValue.js';
19
20
  export * from './usePasswordReset.js';
20
21
  export * from './usePlatform.js';
21
22
  export * from './usePreview.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
@@ -16,6 +16,7 @@ export * from './useHttp.js';
16
16
  export * from './useLogger.js';
17
17
  export * from './useLogin.js';
18
18
  export * from './useOAuth.js';
19
+ export * from './useOptimisticValue.js';
19
20
  export * from './usePasswordReset.js';
20
21
  export * from './usePlatform.js';
21
22
  export * from './usePreview.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
@@ -0,0 +1,67 @@
1
+ import type { OptimisticValueOptions, OptimisticValueState } from '@molecule/app-state';
2
+ /**
3
+ * What {@link useOptimisticValue} returns: the state to render and the two ways to change it.
4
+ */
5
+ export interface UseOptimisticValueResult<T> extends OptimisticValueState<T> {
6
+ /**
7
+ * Shows `next` at once and saves it, after any save already running.
8
+ *
9
+ * @param next - The value the person chose.
10
+ */
11
+ set: (next: T) => void;
12
+ /**
13
+ * Like `set`, computed from the value shown now (`update((on) => !on)`).
14
+ *
15
+ * @param change - Computes the next value from the value shown now.
16
+ */
17
+ update: (change: (current: T) => T) => void;
18
+ }
19
+ /**
20
+ * A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
21
+ * setting switch — that changes on the first render after a tap and is saved one change at a
22
+ * time, in order. A tap during a save is saved right after it; only a failure of the newest
23
+ * change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
24
+ *
25
+ * `value` is the server's value from props or a query. A new `value` is taken only when it
26
+ * differs (by `equals`) from the one the hook last received, and only while nothing is being
27
+ * saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
28
+ * latest `commit` and `onError` are always used. Give the component a `key` per record, so one
29
+ * record's pending change never shows on another.
30
+ *
31
+ * @param options - The server's current value and how to save a change.
32
+ * @returns The value to render, whether a save is pending, the newest failure, and `set`/`update`.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * import { useOptimisticValue } from '@molecule/app-react'
37
+ *
38
+ * function StarButton({ repository }: { repository: Repository }) {
39
+ * const star = useOptimisticValue({
40
+ * value: { starred: repository.starred, stars: repository.stars },
41
+ * equals: (a, b) => a.starred === b.starred && a.stars === b.stars,
42
+ * commit: async (next) => {
43
+ * const response = next.starred
44
+ * ? await http.put(`/repositories/${repository.id}/star`)
45
+ * : await http.delete(`/repositories/${repository.id}/star`)
46
+ * return { starred: response.data.starred, stars: response.data.stars }
47
+ * },
48
+ * })
49
+ * // Never disable the button or return early while `star.pending` is true.
50
+ * return (
51
+ * <button
52
+ * aria-pressed={star.value.starred}
53
+ * onClick={() =>
54
+ * star.update((current) => ({
55
+ * starred: !current.starred,
56
+ * stars: current.stars + (current.starred ? -1 : 1),
57
+ * }))
58
+ * }
59
+ * >
60
+ * {star.value.stars}
61
+ * </button>
62
+ * )
63
+ * }
64
+ * ```
65
+ */
66
+ export declare const useOptimisticValue: <T>(options: OptimisticValueOptions<T>) => UseOptimisticValueResult<T>;
67
+ //# sourceMappingURL=useOptimisticValue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOptimisticValue.d.ts","sourceRoot":"","sources":["../../src/hooks/useOptimisticValue.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAGvF;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC,CAAE,SAAQ,oBAAoB,CAAC,CAAC,CAAC;IAC1E;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAEtB;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,SAAS,sBAAsB,CAAC,CAAC,CAAC,KACjC,wBAAwB,CAAC,CAAC,CA2B5B,CAAA"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * React hook for a server-backed value that the UI changes at once and saves in order.
3
+ *
4
+ * @module
5
+ */
6
+ import { useEffect, useInsertionEffect, useRef, useState, useSyncExternalStore } from 'react';
7
+ import { createOptimisticValue, shallowEqual } from '@molecule/app-state';
8
+ /**
9
+ * A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
10
+ * setting switch — that changes on the first render after a tap and is saved one change at a
11
+ * time, in order. A tap during a save is saved right after it; only a failure of the newest
12
+ * change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
13
+ *
14
+ * `value` is the server's value from props or a query. A new `value` is taken only when it
15
+ * differs (by `equals`) from the one the hook last received, and only while nothing is being
16
+ * saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
17
+ * latest `commit` and `onError` are always used. Give the component a `key` per record, so one
18
+ * record's pending change never shows on another.
19
+ *
20
+ * @param options - The server's current value and how to save a change.
21
+ * @returns The value to render, whether a save is pending, the newest failure, and `set`/`update`.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { useOptimisticValue } from '@molecule/app-react'
26
+ *
27
+ * function StarButton({ repository }: { repository: Repository }) {
28
+ * const star = useOptimisticValue({
29
+ * value: { starred: repository.starred, stars: repository.stars },
30
+ * equals: (a, b) => a.starred === b.starred && a.stars === b.stars,
31
+ * commit: async (next) => {
32
+ * const response = next.starred
33
+ * ? await http.put(`/repositories/${repository.id}/star`)
34
+ * : await http.delete(`/repositories/${repository.id}/star`)
35
+ * return { starred: response.data.starred, stars: response.data.stars }
36
+ * },
37
+ * })
38
+ * // Never disable the button or return early while `star.pending` is true.
39
+ * return (
40
+ * <button
41
+ * aria-pressed={star.value.starred}
42
+ * onClick={() =>
43
+ * star.update((current) => ({
44
+ * starred: !current.starred,
45
+ * stars: current.stars + (current.starred ? -1 : 1),
46
+ * }))
47
+ * }
48
+ * >
49
+ * {star.value.stars}
50
+ * </button>
51
+ * )
52
+ * }
53
+ * ```
54
+ */
55
+ export const useOptimisticValue = (options) => {
56
+ const latest = useRef(options);
57
+ // Exposed actions may run in a descendant's layout effect, before this component's
58
+ // layout/passive effects. Publish options from committed renders before those actions.
59
+ useInsertionEffect(() => {
60
+ latest.current = options;
61
+ });
62
+ const [controller] = useState(() => createOptimisticValue({
63
+ value: options.value,
64
+ commit: (next, previous) => latest.current.commit(next, previous),
65
+ equals: (a, b) => (latest.current.equals ?? shallowEqual)(a, b),
66
+ onError: (error, value) => latest.current.onError?.(error, value),
67
+ }));
68
+ const received = useRef(options.value);
69
+ const { value, equals } = options;
70
+ useEffect(() => {
71
+ if ((equals ?? shallowEqual)(received.current, value))
72
+ return;
73
+ received.current = value;
74
+ controller.sync(value);
75
+ }, [controller, value, equals]);
76
+ const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
77
+ return { ...state, set: controller.set, update: controller.update };
78
+ };
79
+ //# sourceMappingURL=useOptimisticValue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOptimisticValue.js","sourceRoot":"","sources":["../../src/hooks/useOptimisticValue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAG7F,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAqBzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,OAAkC,EACL,EAAE;IAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAC9B,mFAAmF;IACnF,uFAAuF;IACvF,kBAAkB,CAAC,GAAG,EAAE;QACtB,MAAM,CAAC,OAAO,GAAG,OAAO,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CACjC,qBAAqB,CAAI;QACvB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC;QACjE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/D,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC;KAClE,CAAC,CACH,CAAA;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACtC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACjC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM,IAAI,YAAY,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;YAAE,OAAM;QAC7D,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;QACxB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACxB,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IAE/B,MAAM,KAAK,GAAG,oBAAoB,CAAC,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;IAClG,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAA;AACrE,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/app-react",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "React framework bindings for molecule.dev",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "@molecule/app-i18n": "^1.0.1",
32
32
  "@molecule/app-logger": "^1.0.1",
33
33
  "@molecule/app-routing": "^1.0.1",
34
- "@molecule/app-state": "^1.0.1",
34
+ "@molecule/app-state": "^1.1.0",
35
35
  "@molecule/app-storage": "^1.0.1",
36
36
  "@molecule/app-theme": "^1.0.1",
37
37
  "@molecule/app-ui": "^1.0.1",