@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 +78 -2
- package/dist/hooks/useOptimisticValue.d.ts +14 -1
- package/dist/hooks/useOptimisticValue.d.ts.map +1 -1
- package/dist/hooks/useOptimisticValue.js +14 -1
- package/dist/hooks/useOptimisticValue.js.map +1 -1
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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>
|
|
@@ -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`.
|
|
@@ -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 }>({
|
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 }>({
|
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"}
|