@molecule/app-react 3.4.0 → 3.4.1

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-05T08:55:37.068Z
6
+ Generated: 2026-11-06T00:14:54.331Z
7
7
  -->
8
8
 
9
9
  # @molecule/app-react
@@ -36,12 +36,75 @@ also render and can still have a null user. Synchronous SSR renders the
36
36
  fallback for a loading client: initialize a request-scoped client first,
37
37
  or leave the opt-in hold disabled for SSR.
38
38
 
39
+ **Server-backed toggles:** use `useOptimisticValue` for a favorite/follow/like,
40
+ bookmark or settings switch that changes immediately. Keep its flag and count
41
+ in one value. Render that value, update it with a functional updater, and return
42
+ both server fields from `commit`. Do not add a second `useState`, a prop-copy
43
+ effect, or an `if (pending) return`/disabled-on-pending guard: the hook accepts
44
+ taps while saving and serializes saves. It coalesces waiting changes to the
45
+ newest value. Let commit reject on failure so the hook can roll back.
46
+ Mount a keyed control only after its record loads. Keep it mounted through
47
+ refetches of that record. The hook protects pending intent from refetches and
48
+ unchanged old props after a save; it cannot order different server snapshots
49
+ received after saving finishes, or make a missing record ready for a click.
50
+
39
51
  > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
40
52
  > It is written to be read by coding agents as much as by people, and is generated from this
41
53
  > package's source — edit `src/index.ts` JSDoc, not this file.
42
54
 
43
55
  ## Quick Start
44
56
 
57
+ ```tsx
58
+ import { useHttpClient, useOptimisticValue, useTranslation } from '@molecule/app-react'
59
+
60
+ interface FavoriteRecord {
61
+ slug: string
62
+ favorited: boolean
63
+ favoritesCount: number
64
+ }
65
+
66
+ function FavoriteButton({
67
+ article,
68
+ onChanged,
69
+ }: {
70
+ article: FavoriteRecord
71
+ onChanged?: (saved: FavoriteRecord) => void
72
+ }) {
73
+ const http = useHttpClient()
74
+ const { t } = useTranslation()
75
+ const favorite = useOptimisticValue({
76
+ value: { favorited: article.favorited, count: article.favoritesCount },
77
+ commit: async (next) => {
78
+ const path = `/articles/${article.slug}/favorite`
79
+ const response = next.favorited
80
+ ? await http.post<{ article: FavoriteRecord }>(path)
81
+ : await http.delete<{ article: FavoriteRecord }>(path)
82
+ const saved = response.data.article
83
+ onChanged?.(saved)
84
+ return { favorited: saved.favorited, count: saved.favoritesCount }
85
+ },
86
+ })
87
+ return (
88
+ <button
89
+ aria-pressed={favorite.value.favorited}
90
+ aria-label={t('article.favorite', undefined, { defaultValue: 'Favorite article' })}
91
+ onClick={() =>
92
+ favorite.update((current) => ({
93
+ favorited: !current.favorited,
94
+ count: current.count + (current.favorited ? -1 : 1),
95
+ }))
96
+ }
97
+ >
98
+ {favorite.value.count}
99
+ </button>
100
+ )
101
+ }
102
+
103
+ function FavoriteControl({ article }: { article: FavoriteRecord | null }) {
104
+ return article ? <FavoriteButton key={article.slug} article={article} /> : null
105
+ }
106
+ ```
107
+
45
108
  ```tsx
46
109
  const profile = useProfileForm<User, { name: string; email: string }>({
47
110
  defaultValues: { name: '', email: '' },
@@ -2431,7 +2494,8 @@ function useOAuth(config?: {
2431
2494
 
2432
2495
  A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
2433
2496
  setting switch — that changes on the first render after a tap and is saved one change at a
2434
- time, in order. A tap during a save is saved right after it; only a failure of the newest
2497
+ time, in order. Taps during a save update the desired value; the newest waiting value is
2498
+ saved after it (intermediate waiting values coalesce). Only a failure of the newest
2435
2499
  change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
2436
2500
 
2437
2501
  `value` is the server's value from props or a query. A new `value` is taken only when it
@@ -2439,6 +2503,18 @@ differs (by `equals`) from the one the hook last received, and only while nothin
2439
2503
  saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
2440
2504
  latest `commit` and `onError` are always used. Give the component a `key` per record, so one
2441
2505
  record's pending change never shows on another.
2506
+ Keep related fields, such as a favorite flag and its count, in the SAME value. Return both
2507
+ from the server response in `commit`, and publish that response to parent/query caches there
2508
+ if needed. Let commit reject on failure: a catch that returns old data hides the failure.
2509
+ Replace display-state setters and prop-copy effects with this hook's value and `update`.
2510
+ `pending` is for feedback, never a reason to disable the toggle or drop a tap.
2511
+
2512
+ Render/enable the keyed control only once its initial record exists. Keep that control
2513
+ mounted through refetches of the same record. This hook does not fix a click handler that
2514
+ returns on a still-null profile, or a remount that throws its controller away. Refetches
2515
+ during a save cannot replace pending intent; changed server snapshots arriving AFTER saving
2516
+ finishes are accepted and need query-layer ordering if they can be stale. See the module
2517
+ quickstart for a complete favorite button with a server-confirmed count.
2442
2518
 
2443
2519
  ```typescript
2444
2520
  function useOptimisticValue<T>(options: OptimisticValueOptions<T>): UseOptimisticValueResult<T>
@@ -19,7 +19,8 @@ export interface UseOptimisticValueResult<T> extends OptimisticValueState<T> {
19
19
  /**
20
20
  * A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
21
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
22
+ * time, in order. Taps during a save update the desired value; the newest waiting value is
23
+ * saved after it (intermediate waiting values coalesce). Only a failure of the newest
23
24
  * change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
24
25
  *
25
26
  * `value` is the server's value from props or a query. A new `value` is taken only when it
@@ -27,6 +28,18 @@ export interface UseOptimisticValueResult<T> extends OptimisticValueState<T> {
27
28
  * saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
28
29
  * latest `commit` and `onError` are always used. Give the component a `key` per record, so one
29
30
  * record's pending change never shows on another.
31
+ * Keep related fields, such as a favorite flag and its count, in the SAME value. Return both
32
+ * from the server response in `commit`, and publish that response to parent/query caches there
33
+ * if needed. Let commit reject on failure: a catch that returns old data hides the failure.
34
+ * Replace display-state setters and prop-copy effects with this hook's value and `update`.
35
+ * `pending` is for feedback, never a reason to disable the toggle or drop a tap.
36
+ *
37
+ * Render/enable the keyed control only once its initial record exists. Keep that control
38
+ * mounted through refetches of the same record. This hook does not fix a click handler that
39
+ * returns on a still-null profile, or a remount that throws its controller away. Refetches
40
+ * during a save cannot replace pending intent; changed server snapshots arriving AFTER saving
41
+ * finishes are accepted and need query-layer ordering if they can be stale. See the module
42
+ * quickstart for a complete favorite button with a server-confirmed count.
30
43
  *
31
44
  * @param options - The server's current value and how to save a change.
32
45
  * @returns The value to render, whether a save is pending, the newest failure, and `set`/`update`.
@@ -1 +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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,SAAS,sBAAsB,CAAC,CAAC,CAAC,KACjC,wBAAwB,CAAC,CAAC,CA2B5B,CAAA"}
@@ -8,7 +8,8 @@ import { createOptimisticValue, shallowEqual } from '@molecule/app-state';
8
8
  /**
9
9
  * A server-backed value — a favorite, follow, like or bookmark button, a rating, a quantity, a
10
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
11
+ * time, in order. Taps during a save update the desired value; the newest waiting value is
12
+ * saved after it (intermediate waiting values coalesce). Only a failure of the newest
12
13
  * change puts the server value back. See `createOptimisticValue` in `@molecule/app-state`.
13
14
  *
14
15
  * `value` is the server's value from props or a query. A new `value` is taken only when it
@@ -16,6 +17,18 @@ import { createOptimisticValue, shallowEqual } from '@molecule/app-state';
16
17
  * saved, so a parent re-render or a refetch of older data never flips the button mid-save. The
17
18
  * latest `commit` and `onError` are always used. Give the component a `key` per record, so one
18
19
  * record's pending change never shows on another.
20
+ * Keep related fields, such as a favorite flag and its count, in the SAME value. Return both
21
+ * from the server response in `commit`, and publish that response to parent/query caches there
22
+ * if needed. Let commit reject on failure: a catch that returns old data hides the failure.
23
+ * Replace display-state setters and prop-copy effects with this hook's value and `update`.
24
+ * `pending` is for feedback, never a reason to disable the toggle or drop a tap.
25
+ *
26
+ * Render/enable the keyed control only once its initial record exists. Keep that control
27
+ * mounted through refetches of the same record. This hook does not fix a click handler that
28
+ * returns on a still-null profile, or a remount that throws its controller away. Refetches
29
+ * during a save cannot replace pending intent; changed server snapshots arriving AFTER saving
30
+ * finishes are accepted and need query-layer ordering if they can be stale. See the module
31
+ * quickstart for a complete favorite button with a server-confirmed count.
19
32
  *
20
33
  * @param options - The server's current value and how to save a change.
21
34
  * @returns The value to render, whether a save is pending, the newest failure, and `set`/`update`.
@@ -1 +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,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC;QACnF,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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;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,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC;QACnF,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/dist/index.d.ts CHANGED
@@ -27,6 +27,63 @@
27
27
  * fallback for a loading client: initialize a request-scoped client first,
28
28
  * or leave the opt-in hold disabled for SSR.
29
29
  *
30
+ * **Server-backed toggles:** use `useOptimisticValue` for a favorite/follow/like,
31
+ * bookmark or settings switch that changes immediately. Keep its flag and count
32
+ * in one value. Render that value, update it with a functional updater, and return
33
+ * both server fields from `commit`. Do not add a second `useState`, a prop-copy
34
+ * effect, or an `if (pending) return`/disabled-on-pending guard: the hook accepts
35
+ * taps while saving and serializes saves. It coalesces waiting changes to the
36
+ * newest value. Let commit reject on failure so the hook can roll back.
37
+ * Mount a keyed control only after its record loads. Keep it mounted through
38
+ * refetches of that record. The hook protects pending intent from refetches and
39
+ * unchanged old props after a save; it cannot order different server snapshots
40
+ * received after saving finishes, or make a missing record ready for a click.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { useHttpClient, useOptimisticValue, useTranslation } from '@molecule/app-react'
45
+ *
46
+ * interface FavoriteRecord {
47
+ * slug: string
48
+ * favorited: boolean
49
+ * favoritesCount: number
50
+ * }
51
+ *
52
+ * function FavoriteButton({ article, onChanged }: {
53
+ * article: FavoriteRecord
54
+ * onChanged?: (saved: FavoriteRecord) => void
55
+ * }) {
56
+ * const http = useHttpClient()
57
+ * const { t } = useTranslation()
58
+ * const favorite = useOptimisticValue({
59
+ * value: { favorited: article.favorited, count: article.favoritesCount },
60
+ * commit: async (next) => {
61
+ * const path = `/articles/${article.slug}/favorite`
62
+ * const response = next.favorited
63
+ * ? await http.post<{ article: FavoriteRecord }>(path)
64
+ * : await http.delete<{ article: FavoriteRecord }>(path)
65
+ * const saved = response.data.article
66
+ * onChanged?.(saved)
67
+ * return { favorited: saved.favorited, count: saved.favoritesCount }
68
+ * },
69
+ * })
70
+ * return (
71
+ * <button
72
+ * aria-pressed={favorite.value.favorited}
73
+ * aria-label={t('article.favorite', undefined, { defaultValue: 'Favorite article' })}
74
+ * onClick={() => favorite.update((current) => ({
75
+ * favorited: !current.favorited,
76
+ * count: current.count + (current.favorited ? -1 : 1),
77
+ * }))}
78
+ * >{favorite.value.count}</button>
79
+ * )
80
+ * }
81
+ *
82
+ * function FavoriteControl({ article }: { article: FavoriteRecord | null }) {
83
+ * return article ? <FavoriteButton key={article.slug} article={article} /> : null
84
+ * }
85
+ * ```
86
+ *
30
87
  * @example
31
88
  * ```tsx
32
89
  * const profile = useProfileForm<User, { name: string; email: string }>({
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiLG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0OG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -27,6 +27,63 @@
27
27
  * fallback for a loading client: initialize a request-scoped client first,
28
28
  * or leave the opt-in hold disabled for SSR.
29
29
  *
30
+ * **Server-backed toggles:** use `useOptimisticValue` for a favorite/follow/like,
31
+ * bookmark or settings switch that changes immediately. Keep its flag and count
32
+ * in one value. Render that value, update it with a functional updater, and return
33
+ * both server fields from `commit`. Do not add a second `useState`, a prop-copy
34
+ * effect, or an `if (pending) return`/disabled-on-pending guard: the hook accepts
35
+ * taps while saving and serializes saves. It coalesces waiting changes to the
36
+ * newest value. Let commit reject on failure so the hook can roll back.
37
+ * Mount a keyed control only after its record loads. Keep it mounted through
38
+ * refetches of that record. The hook protects pending intent from refetches and
39
+ * unchanged old props after a save; it cannot order different server snapshots
40
+ * received after saving finishes, or make a missing record ready for a click.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { useHttpClient, useOptimisticValue, useTranslation } from '@molecule/app-react'
45
+ *
46
+ * interface FavoriteRecord {
47
+ * slug: string
48
+ * favorited: boolean
49
+ * favoritesCount: number
50
+ * }
51
+ *
52
+ * function FavoriteButton({ article, onChanged }: {
53
+ * article: FavoriteRecord
54
+ * onChanged?: (saved: FavoriteRecord) => void
55
+ * }) {
56
+ * const http = useHttpClient()
57
+ * const { t } = useTranslation()
58
+ * const favorite = useOptimisticValue({
59
+ * value: { favorited: article.favorited, count: article.favoritesCount },
60
+ * commit: async (next) => {
61
+ * const path = `/articles/${article.slug}/favorite`
62
+ * const response = next.favorited
63
+ * ? await http.post<{ article: FavoriteRecord }>(path)
64
+ * : await http.delete<{ article: FavoriteRecord }>(path)
65
+ * const saved = response.data.article
66
+ * onChanged?.(saved)
67
+ * return { favorited: saved.favorited, count: saved.favoritesCount }
68
+ * },
69
+ * })
70
+ * return (
71
+ * <button
72
+ * aria-pressed={favorite.value.favorited}
73
+ * aria-label={t('article.favorite', undefined, { defaultValue: 'Favorite article' })}
74
+ * onClick={() => favorite.update((current) => ({
75
+ * favorited: !current.favorited,
76
+ * count: current.count + (current.favorited ? -1 : 1),
77
+ * }))}
78
+ * >{favorite.value.count}</button>
79
+ * )
80
+ * }
81
+ *
82
+ * function FavoriteControl({ article }: { article: FavoriteRecord | null }) {
83
+ * return article ? <FavoriteButton key={article.slug} article={article} /> : null
84
+ * }
85
+ * ```
86
+ *
30
87
  * @example
31
88
  * ```tsx
32
89
  * const profile = useProfileForm<User, { name: string; email: string }>({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiLG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0OG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/app-react",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "React framework bindings for molecule.dev",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",