react-mnemonic 1.1.0-beta0 → 1.2.1-beta1.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 +41 -748
- package/dist/core.cjs +1322 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +15 -0
- package/dist/core.d.ts +15 -0
- package/dist/core.js +1313 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +337 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2069
- package/dist/index.d.ts +4 -2069
- package/dist/index.js +338 -259
- package/dist/index.js.map +1 -1
- package/dist/key-BvFvcKiR.d.cts +1723 -0
- package/dist/key-BvFvcKiR.d.ts +1723 -0
- package/dist/schema.cjs +2276 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +317 -0
- package/dist/schema.d.ts +317 -0
- package/dist/schema.js +2256 -0
- package/dist/schema.js.map +1 -0
- package/package.json +21 -4
package/README.md
CHANGED
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
# react-mnemonic
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI-friendly, persistent, type-safe state for React.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/react-mnemonic)
|
|
6
6
|
[](https://thirtytwobits.github.io/react-mnemonic/)
|
|
7
|
-
[](https://www.npmjs.com/package/react-mnemonic)
|
|
9
|
-
[](./LICENSE.md)
|
|
10
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/thirtytwobits/react-mnemonic/blob/main/LICENSE.md)
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
page refreshes, synchronize across tabs, and stay type-safe end-to-end -- all
|
|
14
|
-
through a single hook that works like `useState`.
|
|
15
|
-
|
|
16
|
-
`1.1.0-beta0` is the current prerelease on the npm `latest` dist-tag. This minor
|
|
17
|
-
`beta0` cut lands ahead of the `beta1` stabilization milestone, which is
|
|
18
|
-
expected to receive only fixes and polish before the first production release.
|
|
19
|
-
|
|
20
|
-
If you are evaluating alternatives, see the
|
|
21
|
-
[Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
|
|
22
|
-
It publishes reproducible bundle measurements, an AI-friendliness benchmark
|
|
23
|
-
built around one-shot app-building prompts, and controlled SSR/API tradeoff
|
|
24
|
-
notes for `react-mnemonic`, Zustand persist, Jotai `atomWithStorage`,
|
|
25
|
-
`use-local-storage-state`, and `usehooks-ts`.
|
|
26
|
-
|
|
27
|
-
## Features
|
|
28
|
-
|
|
29
|
-
- **`useState`-like API** -- `useMnemonicKey` returns `{ value, set, reset, remove }`
|
|
30
|
-
- **JSON Schema validation** -- optional schema-based validation using a built-in JSON Schema subset
|
|
31
|
-
- **Namespace isolation** -- `MnemonicProvider` prefixes every key to prevent collisions
|
|
32
|
-
- **Cross-tab sync** -- opt-in `listenCrossTab` uses the browser `storage` event
|
|
33
|
-
- **Pluggable storage** -- use `localStorage`, `sessionStorage`, or a synchronous `StorageLike` facade over custom persistence
|
|
34
|
-
- **Schema versioning and migration** -- upgrade stored data with versioned schemas and migration rules
|
|
35
|
-
- **Typed schema cohesion helpers** -- define one schema object and reuse it across runtime validation, key descriptors, and migrations
|
|
36
|
-
- **Structural migration helpers** -- optional tree utilities for idempotent insert/rename/dedupe migration steps
|
|
37
|
-
- **Read-time reconciliation** -- selectively enforce new defaults on persisted values without clearing the whole key
|
|
38
|
-
- **Recovery helpers** -- build user-facing soft reset and hard reset flows with namespace-scoped clear helpers
|
|
39
|
-
- **Write-time normalization** -- migrations where `fromVersion === toVersion` run on every write
|
|
40
|
-
- **First-class key descriptors** -- define canonical, reusable key contracts once with `defineMnemonicKey(...)`
|
|
41
|
-
- **Lifecycle callbacks** -- `onMount` and `onChange` hooks
|
|
42
|
-
- **DevTools** -- inspect and mutate state from the browser console
|
|
43
|
-
- **SSR-safe with explicit controls** -- defaults to `defaultValue` on the server, with optional `ssr.serverValue` and `client-only` hydration
|
|
44
|
-
- **Tree-shakeable, zero dependencies** -- ships ESM + CJS with full TypeScript declarations
|
|
9
|
+
`react-mnemonic` gives your components persistent memory through a hook that feels like `useState`. Values survive reloads, can stay in sync across tabs, and remain SSR-safe by default. It is designed to be AI-friendly, prioritizing visible structure and unambiguous specifications. When you need more than raw storage, the package can validate, version, and migrate persisted data.
|
|
45
10
|
|
|
46
11
|
## Installation
|
|
47
12
|
|
|
@@ -49,34 +14,15 @@ notes for `react-mnemonic`, Zustand persist, Jotai `atomWithStorage`,
|
|
|
49
14
|
npm install react-mnemonic
|
|
50
15
|
```
|
|
51
16
|
|
|
52
|
-
|
|
53
|
-
yarn add react-mnemonic
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
pnpm add react-mnemonic
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Peer dependencies
|
|
61
|
-
|
|
62
|
-
React 18 or later is required. CI verifies packaged-consumer installs against
|
|
63
|
-
React 18 and React 19.
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
{
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"react": ">=18",
|
|
69
|
-
"react-dom": ">=18"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
17
|
+
React 18 or later is required.
|
|
73
18
|
|
|
74
19
|
## Quick start
|
|
75
20
|
|
|
76
|
-
Wrap your app in a `MnemonicProvider`, then call `useMnemonicKey` anywhere
|
|
21
|
+
Wrap your app in a `MnemonicProvider`, then call `useMnemonicKey` anywhere
|
|
22
|
+
inside it.
|
|
77
23
|
|
|
78
24
|
```tsx
|
|
79
|
-
import { MnemonicProvider, useMnemonicKey } from "react-mnemonic";
|
|
25
|
+
import { MnemonicProvider, useMnemonicKey } from "react-mnemonic/core";
|
|
80
26
|
|
|
81
27
|
function Counter() {
|
|
82
28
|
const { value: count, set } = useMnemonicKey("count", {
|
|
@@ -100,701 +46,48 @@ export default function App() {
|
|
|
100
46
|
}
|
|
101
47
|
```
|
|
102
48
|
|
|
103
|
-
|
|
104
|
-
survives full page
|
|
105
|
-
|
|
106
|
-
In server-rendered apps, `useMnemonicKey(...)` renders `defaultValue` on the
|
|
107
|
-
server by default and then hydrates to persisted storage on the client. When
|
|
108
|
-
you need a deterministic server placeholder or want to delay storage reads
|
|
109
|
-
until after mount, use `ssr.serverValue` and `ssr.hydration: "client-only"`.
|
|
110
|
-
See the
|
|
111
|
-
[Server Rendering guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/server-rendering)
|
|
112
|
-
for Next.js and Remix examples.
|
|
113
|
-
|
|
114
|
-
If the same key is used in multiple components, consider defining it once with
|
|
115
|
-
`defineMnemonicKey(...)` and reusing that descriptor everywhere. This keeps the
|
|
116
|
-
contract importable and explicit for both humans and AI-assisted tooling.
|
|
117
|
-
|
|
118
|
-
For a deterministic implementation reference aimed at AI agents and advanced
|
|
119
|
-
users, see the
|
|
120
|
-
[AI docs overview](https://thirtytwobits.github.io/react-mnemonic/docs/ai).
|
|
121
|
-
The canonical agent-facing prose lives under
|
|
122
|
-
[`website/docs/ai/`](https://github.com/thirtytwobits/react-mnemonic/tree/main/website/docs/ai),
|
|
123
|
-
with compact retrieval surfaces published as
|
|
124
|
-
[`llms.txt`](https://thirtytwobits.github.io/react-mnemonic/llms.txt),
|
|
125
|
-
[`llms-full.txt`](https://thirtytwobits.github.io/react-mnemonic/llms-full.txt),
|
|
126
|
-
and
|
|
127
|
-
[`ai-contract.json`](https://thirtytwobits.github.io/react-mnemonic/ai-contract.json).
|
|
128
|
-
Agents should import published types from `react-mnemonic` and must not invent
|
|
129
|
-
local `.d.ts` shims for the package.
|
|
130
|
-
|
|
131
|
-
If you need evidence for where `react-mnemonic` is heavier, more explicit, or
|
|
132
|
-
better suited to SSR-sensitive persistence than lighter hooks, see the
|
|
133
|
-
[Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
|
|
49
|
+
This persists the counter in `localStorage` as `my-app.count`, so the value
|
|
50
|
+
survives a full page reload.
|
|
134
51
|
|
|
135
|
-
|
|
136
|
-
whatever you pass to `set`, so keep transient UI state like loading flags,
|
|
137
|
-
hover state, and draft search text in plain React state unless you explicitly
|
|
138
|
-
want them to rehydrate after reload. See the
|
|
139
|
-
[Persisted vs Ephemeral State guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/persisted-vs-ephemeral-state)
|
|
140
|
-
for patterns and an interactive example.
|
|
52
|
+
## Why use it
|
|
141
53
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
54
|
+
- `useState`-like API: `useMnemonicKey` returns `{ value, set, reset, remove }`
|
|
55
|
+
- Namespaced persistence through `MnemonicProvider`
|
|
56
|
+
- Optional cross-tab synchronization
|
|
57
|
+
- SSR-safe defaults for server-rendered React apps
|
|
58
|
+
- Optional schema validation, versioning, migrations, and reconciliation
|
|
59
|
+
- Zero runtime dependencies with published TypeScript types
|
|
147
60
|
|
|
148
|
-
|
|
149
|
-
`null` explicitly. `remove()` deletes the key and falls back to `defaultValue`,
|
|
150
|
-
while `reset()` writes the default again. See the
|
|
151
|
-
[Clearable Persisted Values guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/clearable-persisted-values)
|
|
152
|
-
for the canonical nullable pattern.
|
|
61
|
+
## Pick the right entrypoint
|
|
153
62
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
and reuse it:
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
import { defineMnemonicKey, useMnemonicKey } from "react-mnemonic";
|
|
161
|
-
|
|
162
|
-
export const themeKey = defineMnemonicKey("theme", {
|
|
163
|
-
defaultValue: "light" as "light" | "dark",
|
|
164
|
-
listenCrossTab: true,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
function ThemeToggle() {
|
|
168
|
-
const { value: theme, set } = useMnemonicKey(themeKey);
|
|
169
|
-
return <button onClick={() => set(theme === "light" ? "dark" : "light")}>{theme}</button>;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function ThemePreview() {
|
|
173
|
-
const { value: theme } = useMnemonicKey(themeKey);
|
|
174
|
-
return <p>Current theme: {theme}</p>;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Descriptors help with:
|
|
179
|
-
|
|
180
|
-
- keeping one canonical key contract per persisted value
|
|
181
|
-
- reusing the same `defaultValue`, `schema`, `codec`, and `reconcile` logic
|
|
182
|
-
- making AI-assisted code generation and refactors less ambiguous
|
|
183
|
-
|
|
184
|
-
The original lightweight form still works:
|
|
185
|
-
|
|
186
|
-
```ts
|
|
187
|
-
const { value, set } = useMnemonicKey("theme", {
|
|
188
|
-
defaultValue: "light" as "light" | "dark",
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## Single source of truth schemas
|
|
193
|
-
|
|
194
|
-
If you want the same schema object to drive both runtime validation and
|
|
195
|
-
TypeScript inference, use the typed schema helpers:
|
|
196
|
-
|
|
197
|
-
```tsx
|
|
198
|
-
import {
|
|
199
|
-
MnemonicProvider,
|
|
200
|
-
createSchemaRegistry,
|
|
201
|
-
defineKeySchema,
|
|
202
|
-
defineMnemonicKey,
|
|
203
|
-
defineMigration,
|
|
204
|
-
mnemonicSchema,
|
|
205
|
-
useMnemonicKey,
|
|
206
|
-
} from "react-mnemonic";
|
|
207
|
-
|
|
208
|
-
const profileV1 = defineKeySchema(
|
|
209
|
-
"profile",
|
|
210
|
-
1,
|
|
211
|
-
mnemonicSchema.object({
|
|
212
|
-
name: mnemonicSchema.string(),
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const profileV2 = defineKeySchema(
|
|
217
|
-
"profile",
|
|
218
|
-
2,
|
|
219
|
-
mnemonicSchema.object({
|
|
220
|
-
name: mnemonicSchema.string(),
|
|
221
|
-
email: mnemonicSchema.string(),
|
|
222
|
-
}),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
const profileKey = defineMnemonicKey(profileV2, {
|
|
226
|
-
defaultValue: { name: "", email: "" },
|
|
227
|
-
reconcile: (value) => ({
|
|
228
|
-
...value,
|
|
229
|
-
email: value.email.trim().toLowerCase(),
|
|
230
|
-
}),
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const registry = createSchemaRegistry({
|
|
234
|
-
schemas: [profileV1, profileV2],
|
|
235
|
-
migrations: [
|
|
236
|
-
defineMigration(profileV1, profileV2, (value) => ({
|
|
237
|
-
...value,
|
|
238
|
-
email: "",
|
|
239
|
-
})),
|
|
240
|
-
],
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
function ProfileForm() {
|
|
244
|
-
const { value: profile, set } = useMnemonicKey(profileKey);
|
|
245
|
-
return <button onClick={() => set({ ...profile, email: "hello@example.com" })}>Save</button>;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export default function App() {
|
|
249
|
-
return (
|
|
250
|
-
<MnemonicProvider namespace="my-app" schemaMode="default" schemaRegistry={registry}>
|
|
251
|
-
<ProfileForm />
|
|
252
|
-
</MnemonicProvider>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
This path gives you:
|
|
258
|
-
|
|
259
|
-
- one schema object reused in the registry, key descriptor, and migrations
|
|
260
|
-
- inferred types for `defaultValue`, `value`, `set`, and `reconcile`
|
|
261
|
-
- typed migration callbacks via `defineMigration(...)`
|
|
262
|
-
|
|
263
|
-
Tradeoffs versus the lightweight JSON-only path:
|
|
264
|
-
|
|
265
|
-
- more setup upfront
|
|
266
|
-
- best fit when a key is schema-managed and long-lived
|
|
267
|
-
- not necessary for simple keys where `defaultValue` inference is already enough
|
|
268
|
-
|
|
269
|
-
For a side-by-side comparison against competing persistence libraries, including
|
|
270
|
-
bundle-size measurements and qualitative SSR/API notes, see the
|
|
271
|
-
[Comparison Guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/comparisons-and-benchmarks).
|
|
272
|
-
|
|
273
|
-
## API
|
|
274
|
-
|
|
275
|
-
### `<MnemonicProvider>`
|
|
276
|
-
|
|
277
|
-
Context provider that scopes storage keys under a namespace.
|
|
278
|
-
|
|
279
|
-
```tsx
|
|
280
|
-
<MnemonicProvider
|
|
281
|
-
namespace="my-app" // key prefix (required)
|
|
282
|
-
storage={localStorage} // StorageLike backend (default: localStorage)
|
|
283
|
-
schemaMode="default" // "default" | "strict" | "autoschema" (default: "default")
|
|
284
|
-
schemaRegistry={registry} // optional SchemaRegistry for versioned schemas
|
|
285
|
-
enableDevTools={false} // expose console helpers (default: false)
|
|
286
|
-
>
|
|
287
|
-
{children}
|
|
288
|
-
</MnemonicProvider>
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
Multiple providers with different namespaces can coexist in the same app.
|
|
292
|
-
|
|
293
|
-
### `defineMnemonicKey(key, options)`
|
|
294
|
-
|
|
295
|
-
Define a reusable descriptor for a single persisted key.
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
const themeKey = defineMnemonicKey("theme", {
|
|
299
|
-
defaultValue: "light" as "light" | "dark",
|
|
300
|
-
listenCrossTab: true,
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
The returned descriptor can be imported and passed directly to `useMnemonicKey`.
|
|
305
|
-
|
|
306
|
-
### `mnemonicSchema`, `defineKeySchema`, and `defineMigration`
|
|
307
|
-
|
|
308
|
-
Use these helpers when you want a schema-managed key to have one source of
|
|
309
|
-
truth for runtime validation and TypeScript inference.
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
const themeSchema = defineKeySchema("theme", 1, mnemonicSchema.enum(["light", "dark"] as const));
|
|
313
|
-
|
|
314
|
-
const themeKey = defineMnemonicKey(themeSchema, {
|
|
315
|
-
defaultValue: "light",
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
`defineMigration(fromSchema, toSchema, migrate)` infers the migration callback
|
|
320
|
-
from the source and target schemas, while `defineWriteMigration(schema, migrate)`
|
|
321
|
-
creates a typed same-version normalizer.
|
|
322
|
-
|
|
323
|
-
### `useMnemonicKey<T>(descriptor)` / `useMnemonicKey<T>(key, options)`
|
|
324
|
-
|
|
325
|
-
Hook for reading and writing a single persistent value.
|
|
326
|
-
|
|
327
|
-
```ts
|
|
328
|
-
const { value, set, reset, remove } = useMnemonicKey(themeKey);
|
|
329
|
-
const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
| Return | Type | Description |
|
|
333
|
-
| -------- | ------------------------------------ | --------------------------------------------- |
|
|
334
|
-
| `value` | `T` | Current decoded value (or default) |
|
|
335
|
-
| `set` | `(next: T \| (cur: T) => T) => void` | Update the value (direct or updater function) |
|
|
336
|
-
| `reset` | `() => void` | Reset to `defaultValue` and persist it |
|
|
337
|
-
| `remove` | `() => void` | Delete the key from storage entirely |
|
|
338
|
-
|
|
339
|
-
For clearable fields, remember the semantic split:
|
|
340
|
-
|
|
341
|
-
- `set(null)` persists a cleared value and stays cleared after reload
|
|
342
|
-
- `remove()` deletes the key, so the next read falls back to `defaultValue`
|
|
343
|
-
- `reset()` persists `defaultValue`
|
|
344
|
-
|
|
345
|
-
#### Options
|
|
346
|
-
|
|
347
|
-
| Option | Type | Default | Description |
|
|
348
|
-
| ---------------- | ------------------------------------------------- | ----------- | ------------------------------------------------------------- |
|
|
349
|
-
| `defaultValue` | `T \| ((error?: CodecError \| SchemaError) => T)` | _required_ | Fallback value or error-aware factory |
|
|
350
|
-
| `codec` | `Codec<T>` | `JSONCodec` | Encode/decode strategy (bypasses schema validation) |
|
|
351
|
-
| `reconcile` | `(value: T, context: ReconcileContext) => T` | -- | Adjust a persisted value after read and persist if it changes |
|
|
352
|
-
| `onMount` | `(value: T) => void` | -- | Called once with the initial value |
|
|
353
|
-
| `onChange` | `(value: T, prev: T) => void` | -- | Called on every value change |
|
|
354
|
-
| `listenCrossTab` | `boolean` | `false` | Sync via the browser `storage` event |
|
|
355
|
-
| `schema` | `{ version?: number }` | -- | Pin writes to a specific schema version |
|
|
356
|
-
|
|
357
|
-
### `useMnemonicRecovery(options)`
|
|
358
|
-
|
|
359
|
-
Hook for namespace-scoped recovery actions such as "clear saved filters" or
|
|
360
|
-
"reset all persisted app data".
|
|
361
|
-
|
|
362
|
-
```ts
|
|
363
|
-
const { namespace, canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching } = useMnemonicRecovery({
|
|
364
|
-
onRecover: (event) => console.log(event.action, event.clearedKeys),
|
|
365
|
-
});
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
| Return | Type | Description |
|
|
369
|
-
| ------------------ | --------------------------------------------------- | ----------------------------------------------------- |
|
|
370
|
-
| `namespace` | `string` | Current provider namespace |
|
|
371
|
-
| `canEnumerateKeys` | `boolean` | Whether the storage backend can list namespace keys |
|
|
372
|
-
| `listKeys` | `() => string[]` | List visible unprefixed keys in the current namespace |
|
|
373
|
-
| `clearAll` | `() => string[]` | Clear every key in the namespace |
|
|
374
|
-
| `clearKeys` | `(keys: readonly string[]) => string[]` | Clear an explicit set of unprefixed keys |
|
|
375
|
-
| `clearMatching` | `(predicate: (key: string) => boolean) => string[]` | Clear keys whose names match a predicate |
|
|
376
|
-
|
|
377
|
-
`clearAll()` and `clearMatching()` require an enumerable storage backend such
|
|
378
|
-
as `localStorage` or `sessionStorage`. If your custom storage does not support
|
|
379
|
-
`length` and `key(index)`, use `clearKeys([...])` with the explicit durable-key
|
|
380
|
-
list your app owns.
|
|
381
|
-
|
|
382
|
-
### Codecs
|
|
383
|
-
|
|
384
|
-
The default codec is `JSONCodec`, which handles all JSON-serializable values.
|
|
385
|
-
You can create custom codecs using `createCodec` for types that need special
|
|
386
|
-
serialization (e.g., `Date`, `Set`, `Map`).
|
|
387
|
-
|
|
388
|
-
Using a custom codec bypasses JSON Schema validation -- the codec is a low-level
|
|
389
|
-
escape hatch for when you need full control over serialization.
|
|
390
|
-
|
|
391
|
-
```ts
|
|
392
|
-
import { createCodec } from "react-mnemonic";
|
|
393
|
-
|
|
394
|
-
const DateCodec = createCodec<Date>(
|
|
395
|
-
(date) => date.toISOString(),
|
|
396
|
-
(str) => new Date(str),
|
|
397
|
-
);
|
|
398
|
-
```
|
|
63
|
+
- `react-mnemonic/core` for the lean persisted-state path
|
|
64
|
+
- `react-mnemonic/schema` when you want schemas, validation, and migrations
|
|
65
|
+
- `react-mnemonic` if you need the backward-compatible root entrypoint
|
|
399
66
|
|
|
400
|
-
|
|
67
|
+
## AI resources
|
|
401
68
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
readonly length?: number;
|
|
411
|
-
onExternalChange?: (callback: (changedKeys?: string[]) => void) => () => void;
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
`StorageLike` is intentionally synchronous in v1 because Mnemonic's core
|
|
416
|
-
store is built on React's synchronous snapshot contract. Async backends are
|
|
417
|
-
still possible, but they need a synchronous facade: keep an in-memory cache for
|
|
418
|
-
`getItem`/`setItem`/`removeItem`, then flush to IndexedDB or another async
|
|
419
|
-
system outside the hook contract. Promise-returning `StorageLike` methods are
|
|
420
|
-
unsupported and are treated as a storage misuse fallback at runtime.
|
|
421
|
-
|
|
422
|
-
`onExternalChange` enables cross-tab sync for non-localStorage backends (e.g.
|
|
423
|
-
IndexedDB over `BroadcastChannel`). The library handles all error cases
|
|
424
|
-
internally -- see the `StorageLike` JSDoc for the full error-handling contract.
|
|
425
|
-
Namespace-wide recovery helpers can only enumerate keys when the backend also
|
|
426
|
-
implements `length` and `key(index)`.
|
|
427
|
-
|
|
428
|
-
### `validateJsonSchema(schema, value)`
|
|
429
|
-
|
|
430
|
-
Validate an arbitrary value against a JSON Schema (the same subset used by the
|
|
431
|
-
hook). Returns an array of validation errors, empty when the value is valid.
|
|
432
|
-
|
|
433
|
-
```ts
|
|
434
|
-
import { validateJsonSchema } from "react-mnemonic";
|
|
435
|
-
|
|
436
|
-
const errors = validateJsonSchema(
|
|
437
|
-
{ type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
|
438
|
-
{ name: 42 },
|
|
439
|
-
);
|
|
440
|
-
// [{ path: ".name", message: 'Expected type "string"' }]
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### `compileSchema(schema)`
|
|
444
|
-
|
|
445
|
-
Pre-compile a JSON Schema into a reusable validator function. The compiled
|
|
446
|
-
validator is cached by schema reference (via `WeakMap`), so calling
|
|
447
|
-
`compileSchema` twice with the same object returns the identical function.
|
|
448
|
-
|
|
449
|
-
```ts
|
|
450
|
-
import { compileSchema } from "react-mnemonic";
|
|
451
|
-
import type { CompiledValidator } from "react-mnemonic";
|
|
452
|
-
|
|
453
|
-
const validate: CompiledValidator = compileSchema({
|
|
454
|
-
type: "object",
|
|
455
|
-
properties: {
|
|
456
|
-
name: { type: "string", minLength: 1 },
|
|
457
|
-
age: { type: "number", minimum: 0 },
|
|
458
|
-
},
|
|
459
|
-
required: ["name"],
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
validate({ name: "Alice", age: 30 }); // []
|
|
463
|
-
validate({ age: -1 }); // [{ path: "", … }, { path: ".age", … }]
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
This is useful when you validate the same schema frequently outside of the hook
|
|
467
|
-
(e.g. in form validation or server responses).
|
|
468
|
-
|
|
469
|
-
### Error classes
|
|
470
|
-
|
|
471
|
-
| Class | Thrown when |
|
|
472
|
-
| ------------- | ------------------------------------ |
|
|
473
|
-
| `CodecError` | Encoding or decoding fails |
|
|
474
|
-
| `SchemaError` | Schema validation or migration fails |
|
|
475
|
-
|
|
476
|
-
Both are passed to `defaultValue` factories so you can inspect or log the
|
|
477
|
-
failure reason.
|
|
478
|
-
|
|
479
|
-
## Usage examples
|
|
480
|
-
|
|
481
|
-
### Cross-tab theme sync
|
|
482
|
-
|
|
483
|
-
```tsx
|
|
484
|
-
const { value: theme, set } = useMnemonicKey<"light" | "dark">("theme", {
|
|
485
|
-
defaultValue: "light",
|
|
486
|
-
listenCrossTab: true,
|
|
487
|
-
onChange: (t) => {
|
|
488
|
-
document.documentElement.setAttribute("data-theme", t);
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
### Error-aware defaults
|
|
494
|
-
|
|
495
|
-
```tsx
|
|
496
|
-
import { useMnemonicKey, CodecError, SchemaError } from "react-mnemonic";
|
|
497
|
-
|
|
498
|
-
const getDefault = (error?: CodecError | SchemaError) => {
|
|
499
|
-
if (error instanceof CodecError) {
|
|
500
|
-
console.warn("Corrupt stored data:", error.message);
|
|
501
|
-
}
|
|
502
|
-
if (error instanceof SchemaError) {
|
|
503
|
-
console.warn("Schema validation failed:", error.message);
|
|
504
|
-
}
|
|
505
|
-
return { count: 0 };
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
const { value } = useMnemonicKey("counter", { defaultValue: getDefault });
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
## Schema modes and versioning
|
|
512
|
-
|
|
513
|
-
Mnemonic supports optional schema versioning through `schemaMode` and an
|
|
514
|
-
optional `schemaRegistry`.
|
|
515
|
-
|
|
516
|
-
- `default`: Schemas are optional. Reads use a schema when one exists for the
|
|
517
|
-
stored version, otherwise the hook codec. Writes use the highest registered
|
|
518
|
-
schema for the key; if no schemas are registered, writes use an unversioned
|
|
519
|
-
(v0) envelope.
|
|
520
|
-
- `strict`: Every stored version must have a registered schema. Reads without a
|
|
521
|
-
matching schema fall back to `defaultValue` with a `SchemaError`.
|
|
522
|
-
Writes require a registered schema when any schemas exist, but fall back to
|
|
523
|
-
a v0 envelope when the registry has none.
|
|
524
|
-
- `autoschema`: Like `default`, but if no schema exists for a key, the first
|
|
525
|
-
successful read infers and registers a v1 schema. Subsequent reads/writes use
|
|
526
|
-
that schema.
|
|
527
|
-
|
|
528
|
-
Version `0` is valid for schemas and migrations. Schemas at version `0` are
|
|
529
|
-
treated like any other version.
|
|
530
|
-
|
|
531
|
-
### JSON Schema validation
|
|
532
|
-
|
|
533
|
-
Schemas use a subset of JSON Schema for validation. The supported keywords are:
|
|
534
|
-
|
|
535
|
-
- `type` (including array form for nullable types, e.g., `["string", "null"]`)
|
|
536
|
-
- `enum`, `const`
|
|
537
|
-
- `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
|
|
538
|
-
- `minLength`, `maxLength`
|
|
539
|
-
- `properties`, `required`, `additionalProperties`
|
|
540
|
-
- `items`, `minItems`, `maxItems`
|
|
541
|
-
|
|
542
|
-
```ts
|
|
543
|
-
// Schema definition -- fully serializable JSON, no functions
|
|
544
|
-
const schema: KeySchema = {
|
|
545
|
-
key: "profile",
|
|
546
|
-
version: 1,
|
|
547
|
-
schema: {
|
|
548
|
-
type: "object",
|
|
549
|
-
properties: {
|
|
550
|
-
name: { type: "string", minLength: 1 },
|
|
551
|
-
email: { type: "string" },
|
|
552
|
-
age: { type: "number", minimum: 0 },
|
|
553
|
-
},
|
|
554
|
-
required: ["name", "email"],
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
### Write-time migrations (normalizers)
|
|
560
|
-
|
|
561
|
-
A migration where `fromVersion === toVersion` runs on every write, acting as a
|
|
562
|
-
normalizer. This is useful for trimming whitespace, lowercasing strings, etc.
|
|
563
|
-
|
|
564
|
-
```ts
|
|
565
|
-
const normalizer: MigrationRule = {
|
|
566
|
-
key: "name",
|
|
567
|
-
fromVersion: 1,
|
|
568
|
-
toVersion: 1,
|
|
569
|
-
migrate: (value) => String(value).trim().toLowerCase(),
|
|
570
|
-
};
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
### Reconciliation
|
|
574
|
-
|
|
575
|
-
Use `reconcile` when you want to keep persisted data but selectively enforce
|
|
576
|
-
new application defaults after the value has been decoded and any read-time
|
|
577
|
-
migrations have already run.
|
|
578
|
-
|
|
579
|
-
```ts
|
|
580
|
-
const { value } = useMnemonicKey("preferences", {
|
|
581
|
-
defaultValue: { theme: "dark", density: "comfortable", accents: true },
|
|
582
|
-
reconcile: (persisted, { persistedVersion }) => ({
|
|
583
|
-
...persisted,
|
|
584
|
-
accents: persistedVersion === 0 ? true : persisted.accents,
|
|
585
|
-
}),
|
|
586
|
-
});
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
Use a schema migration when the stored shape must move from one explicit version
|
|
590
|
-
to another. Use `reconcile` for conditional, field-level policy changes such as
|
|
591
|
-
rolling out a new default while preserving the rest of a user's stored data.
|
|
592
|
-
|
|
593
|
-
### Structural migration helpers
|
|
594
|
-
|
|
595
|
-
For layout-like data that already uses `id` and `children`, Mnemonic ships
|
|
596
|
-
optional pure helpers for common idempotent migration steps:
|
|
597
|
-
|
|
598
|
-
```ts
|
|
599
|
-
import { insertChildIfMissing, renameNode, dedupeChildrenBy } from "react-mnemonic";
|
|
600
|
-
|
|
601
|
-
const migrated = dedupeChildrenBy(
|
|
602
|
-
renameNode(insertChildIfMissing(layout, "sidebar", { id: "search", title: "Search" }), "prefs", "preferences"),
|
|
603
|
-
(node) => node.id,
|
|
604
|
-
);
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
Use these inside your `MigrationRule.migrate` functions when you want repeatable
|
|
608
|
-
tree edits without hand-writing the same traversal logic each time. See the
|
|
609
|
-
[Schema Migration guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/schema-migration)
|
|
610
|
-
for a cookbook example and custom adapter usage.
|
|
611
|
-
|
|
612
|
-
### Example schema registry
|
|
613
|
-
|
|
614
|
-
A schema registry stores versioned schemas for each key, and resolves migration
|
|
615
|
-
paths to upgrade stored data. For the common immutable case, use
|
|
616
|
-
`createSchemaRegistry(...)` instead of hand-rolling the indexing boilerplate.
|
|
617
|
-
|
|
618
|
-
```tsx
|
|
619
|
-
import {
|
|
620
|
-
createSchemaRegistry,
|
|
621
|
-
MnemonicProvider,
|
|
622
|
-
useMnemonicKey,
|
|
623
|
-
type KeySchema,
|
|
624
|
-
type MigrationRule,
|
|
625
|
-
} from "react-mnemonic";
|
|
626
|
-
|
|
627
|
-
const schemas: KeySchema[] = [
|
|
628
|
-
{
|
|
629
|
-
key: "profile",
|
|
630
|
-
version: 1,
|
|
631
|
-
schema: {
|
|
632
|
-
type: "object",
|
|
633
|
-
properties: { name: { type: "string" }, email: { type: "string" } },
|
|
634
|
-
required: ["name", "email"],
|
|
635
|
-
},
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
key: "profile",
|
|
639
|
-
version: 2,
|
|
640
|
-
schema: {
|
|
641
|
-
type: "object",
|
|
642
|
-
properties: {
|
|
643
|
-
name: { type: "string" },
|
|
644
|
-
email: { type: "string" },
|
|
645
|
-
migratedAt: { type: "string" },
|
|
646
|
-
},
|
|
647
|
-
required: ["name", "email", "migratedAt"],
|
|
648
|
-
},
|
|
649
|
-
},
|
|
650
|
-
];
|
|
651
|
-
|
|
652
|
-
const migrations: MigrationRule[] = [
|
|
653
|
-
{
|
|
654
|
-
key: "profile",
|
|
655
|
-
fromVersion: 1,
|
|
656
|
-
toVersion: 2,
|
|
657
|
-
migrate: (value) => {
|
|
658
|
-
const v1 = value as { name: string; email: string };
|
|
659
|
-
return { ...v1, migratedAt: new Date().toISOString() };
|
|
660
|
-
},
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
key: "profile",
|
|
664
|
-
fromVersion: 2,
|
|
665
|
-
toVersion: 2,
|
|
666
|
-
migrate: (value) => {
|
|
667
|
-
const profile = value as { name: string; email: string; migratedAt: string };
|
|
668
|
-
return { ...profile, email: profile.email.trim().toLowerCase() };
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
];
|
|
672
|
-
|
|
673
|
-
const registry = createSchemaRegistry({
|
|
674
|
-
schemas,
|
|
675
|
-
migrations,
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
function ProfileEditor() {
|
|
679
|
-
const { value, set } = useMnemonicKey<{ name: string; email: string; migratedAt: string }>("profile", {
|
|
680
|
-
defaultValue: { name: "", email: "", migratedAt: "" },
|
|
681
|
-
});
|
|
682
|
-
return <input value={value.name} onChange={(e) => set({ ...value, name: e.target.value })} />;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
<MnemonicProvider namespace="app" schemaMode="default" schemaRegistry={registry}>
|
|
686
|
-
<ProfileEditor />
|
|
687
|
-
</MnemonicProvider>;
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
`createSchemaRegistry` validates duplicate schemas and ambiguous migration
|
|
691
|
-
graphs up front. If you need runtime schema registration for
|
|
692
|
-
`schemaMode="autoschema"`, keep a custom mutable `SchemaRegistry`
|
|
693
|
-
implementation.
|
|
694
|
-
|
|
695
|
-
### Registry immutability
|
|
696
|
-
|
|
697
|
-
In `default` and `strict` modes, the schema registry is treated as immutable for
|
|
698
|
-
the lifetime of the provider. The hook caches registry lookups to keep read and
|
|
699
|
-
write hot paths fast. To ship new schemas or migrations, publish a new app
|
|
700
|
-
version and remount the provider.
|
|
701
|
-
|
|
702
|
-
`autoschema` remains mutable because inferred schemas are registered at runtime.
|
|
703
|
-
|
|
704
|
-
### Custom storage backend
|
|
705
|
-
|
|
706
|
-
```tsx
|
|
707
|
-
import { MnemonicProvider } from "react-mnemonic";
|
|
708
|
-
import type { StorageLike } from "react-mnemonic";
|
|
709
|
-
|
|
710
|
-
const cache = new Map<string, string>();
|
|
711
|
-
const queueIndexedDbWrite = (key: string, value: string) => {
|
|
712
|
-
// application-specific async persistence
|
|
713
|
-
};
|
|
714
|
-
const queueIndexedDbDelete = (key: string) => {
|
|
715
|
-
// application-specific async persistence
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
const idbStorage: StorageLike = {
|
|
719
|
-
getItem: (key) => cache.get(key) ?? null,
|
|
720
|
-
setItem: (key, value) => {
|
|
721
|
-
cache.set(key, value);
|
|
722
|
-
queueIndexedDbWrite(key, value);
|
|
723
|
-
},
|
|
724
|
-
removeItem: (key) => {
|
|
725
|
-
cache.delete(key);
|
|
726
|
-
queueIndexedDbDelete(key);
|
|
727
|
-
},
|
|
728
|
-
onExternalChange: (cb) => {
|
|
729
|
-
const bc = new BroadcastChannel("my-app-sync");
|
|
730
|
-
bc.onmessage = (e) => cb(e.data.keys);
|
|
731
|
-
return () => bc.close();
|
|
732
|
-
},
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
<MnemonicProvider namespace="my-app" storage={idbStorage}>
|
|
736
|
-
<App />
|
|
737
|
-
</MnemonicProvider>;
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
If your real persistence layer is async, initialize that adapter before
|
|
741
|
-
rendering the provider and return a synchronous `StorageLike` facade, like the
|
|
742
|
-
IndexedDB demo in the docs site.
|
|
743
|
-
|
|
744
|
-
### DevTools
|
|
745
|
-
|
|
746
|
-
Enable the console inspector in development:
|
|
747
|
-
|
|
748
|
-
```tsx
|
|
749
|
-
<MnemonicProvider namespace="app" enableDevTools={process.env.NODE_ENV === "development"}>
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
Then in the browser console:
|
|
753
|
-
|
|
754
|
-
```js
|
|
755
|
-
const app = window.__REACT_MNEMONIC_DEVTOOLS__?.resolve("app");
|
|
756
|
-
|
|
757
|
-
app?.dump(); // table of all keys
|
|
758
|
-
app?.get("theme"); // read a decoded value
|
|
759
|
-
app?.set("theme", "dark"); // write
|
|
760
|
-
app?.remove("theme"); // delete
|
|
761
|
-
app?.keys(); // list all keys
|
|
762
|
-
app?.clear(); // remove all keys
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
## TypeScript
|
|
766
|
-
|
|
767
|
-
The library is written in strict TypeScript and ships its own declarations.
|
|
768
|
-
All public types are re-exported from the package root:
|
|
769
|
-
|
|
770
|
-
```ts
|
|
771
|
-
import type {
|
|
772
|
-
Codec,
|
|
773
|
-
StorageLike,
|
|
774
|
-
MnemonicKeyDescriptor,
|
|
775
|
-
MnemonicProviderOptions,
|
|
776
|
-
MnemonicProviderProps,
|
|
777
|
-
UseMnemonicKeyOptions,
|
|
778
|
-
KeySchema,
|
|
779
|
-
MigrationRule,
|
|
780
|
-
MigrationPath,
|
|
781
|
-
SchemaRegistry,
|
|
782
|
-
SchemaMode,
|
|
783
|
-
JsonSchema,
|
|
784
|
-
JsonSchemaType,
|
|
785
|
-
JsonSchemaValidationError,
|
|
786
|
-
CompiledValidator,
|
|
787
|
-
} from "react-mnemonic";
|
|
788
|
-
```
|
|
69
|
+
| Resource | Purpose |
|
|
70
|
+
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
71
|
+
| [AI Docs](https://thirtytwobits.github.io/react-mnemonic/docs/ai) | Canonical invariants, decision matrix, recipes, anti-patterns, and setup guidance |
|
|
72
|
+
| [`llms.txt`](https://thirtytwobits.github.io/react-mnemonic/llms.txt) | Compact retrieval index for tight context windows |
|
|
73
|
+
| [`llms-full.txt`](https://thirtytwobits.github.io/react-mnemonic/llms-full.txt) | Long-form export for indexing and larger prompt contexts |
|
|
74
|
+
| [`ai-contract.json`](https://thirtytwobits.github.io/react-mnemonic/ai-contract.json) | Machine-readable persistence contract for tooling and agent integrations |
|
|
75
|
+
| [DeepWiki priorities](https://github.com/thirtytwobits/react-mnemonic/blob/main/.devin/wiki.json) | Steering file that points DeepWiki toward the highest-signal sources |
|
|
76
|
+
| [AI Assistant Setup](https://thirtytwobits.github.io/react-mnemonic/docs/ai/assistant-setup) | Generated instruction packs plus the documented MCP-friendly retrieval path |
|
|
789
77
|
|
|
790
|
-
##
|
|
78
|
+
## Learn more
|
|
791
79
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
80
|
+
- [Documentation home](https://thirtytwobits.github.io/react-mnemonic/)
|
|
81
|
+
- [Quick Start](https://thirtytwobits.github.io/react-mnemonic/docs/getting-started/quick-start)
|
|
82
|
+
- [Server Rendering](https://thirtytwobits.github.io/react-mnemonic/docs/guides/server-rendering)
|
|
83
|
+
- [Canonical Key Definitions](https://thirtytwobits.github.io/react-mnemonic/docs/guides/canonical-key-definitions)
|
|
84
|
+
- [Single Source of Truth Schemas](https://thirtytwobits.github.io/react-mnemonic/docs/guides/single-source-of-truth-schemas)
|
|
85
|
+
- [Schema Migration](https://thirtytwobits.github.io/react-mnemonic/docs/guides/schema-migration)
|
|
86
|
+
- [Auth-Aware Persistence](https://thirtytwobits.github.io/react-mnemonic/docs/guides/auth-aware-persistence)
|
|
87
|
+
- [Context7 Rankings](https://thirtytwobits.github.io/react-mnemonic/docs/guides/context7-rankings)
|
|
88
|
+
- [API Reference](https://thirtytwobits.github.io/react-mnemonic/docs/api)
|
|
89
|
+
- [AI Overview](https://thirtytwobits.github.io/react-mnemonic/docs/ai)
|
|
797
90
|
|
|
798
91
|
## License
|
|
799
92
|
|
|
800
|
-
[MIT](
|
|
93
|
+
[MIT](https://github.com/thirtytwobits/react-mnemonic/blob/main/LICENSE.md)
|