@molecule/app-react 3.3.1 → 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 +83 -3
- package/dist/hooks/useOptimisticValue.d.ts +14 -1
- package/dist/hooks/useOptimisticValue.d.ts.map +1 -1
- package/dist/hooks/useOptimisticValue.js +15 -2
- package/dist/hooks/useOptimisticValue.js.map +1 -1
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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-
|
|
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.
|
|
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>
|
|
@@ -3326,7 +3402,7 @@ Peer dependencies:
|
|
|
3326
3402
|
- `@molecule/app-i18n` ^1.0.1
|
|
3327
3403
|
- `@molecule/app-logger` ^1.0.1
|
|
3328
3404
|
- `@molecule/app-routing` ^1.0.1
|
|
3329
|
-
- `@molecule/app-state` ^1.
|
|
3405
|
+
- `@molecule/app-state` ^1.2.0
|
|
3330
3406
|
- `@molecule/app-storage` ^1.0.1
|
|
3331
3407
|
- `@molecule/app-theme` ^1.0.1
|
|
3332
3408
|
- `@molecule/app-ui` ^1.0.1
|
|
@@ -3364,6 +3440,10 @@ Peer dependencies:
|
|
|
3364
3440
|
- `@molecule/app-version`
|
|
3365
3441
|
- `react`
|
|
3366
3442
|
|
|
3443
|
+
- Server-backed switches and counters use `useOptimisticValue`. When one commit saves
|
|
3444
|
+
several records, call its third argument after each accepted request:
|
|
3445
|
+
`confirm((current) => ({ ...current, [id]: saved }))`, then return nothing. A later
|
|
3446
|
+
rejection keeps those accepted records while rolling back the unsaved changes.
|
|
3367
3447
|
- `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
3368
3448
|
and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
3369
3449
|
and `toValues` must cover every registered control; map nullable text to
|
|
@@ -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.
|
|
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
|
|
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.
|
|
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`.
|
|
@@ -61,7 +74,7 @@ export const useOptimisticValue = (options) => {
|
|
|
61
74
|
});
|
|
62
75
|
const [controller] = useState(() => createOptimisticValue({
|
|
63
76
|
value: options.value,
|
|
64
|
-
commit: (next, previous) => latest.current.commit(next, previous),
|
|
77
|
+
commit: (next, previous, confirm) => latest.current.commit(next, previous, confirm),
|
|
65
78
|
equals: (a, b) => (latest.current.equals ?? shallowEqual)(a, b),
|
|
66
79
|
onError: (error, value) => latest.current.onError?.(error, value),
|
|
67
80
|
}));
|
|
@@ -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
|
|
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 }>({
|
|
@@ -90,6 +147,10 @@
|
|
|
90
147
|
* ```
|
|
91
148
|
*
|
|
92
149
|
* @remarks
|
|
150
|
+
* - Server-backed switches and counters use `useOptimisticValue`. When one commit saves
|
|
151
|
+
* several records, call its third argument after each accepted request:
|
|
152
|
+
* `confirm((current) => ({ ...current, [id]: saved }))`, then return nothing. A later
|
|
153
|
+
* rejection keeps those accepted records while rolling back the unsaved changes.
|
|
93
154
|
* - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
94
155
|
* and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
95
156
|
* and `toValues` must cover every registered control; map nullable text to
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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 }>({
|
|
@@ -90,6 +147,10 @@
|
|
|
90
147
|
* ```
|
|
91
148
|
*
|
|
92
149
|
* @remarks
|
|
150
|
+
* - Server-backed switches and counters use `useOptimisticValue`. When one commit saves
|
|
151
|
+
* several records, call its third argument after each accepted request:
|
|
152
|
+
* `confirm((current) => ({ ...current, [id]: saved }))`, then return nothing. A later
|
|
153
|
+
* rejection keeps those accepted records while rolling back the unsaved changes.
|
|
93
154
|
* - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
94
155
|
* and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
95
156
|
* and `toValues` must cover every registered control; map nullable text to
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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.
|
|
3
|
+
"version": "3.4.1",
|
|
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.
|
|
34
|
+
"@molecule/app-state": "^1.2.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",
|