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 CHANGED
@@ -1,47 +1,12 @@
1
1
  # react-mnemonic
2
2
 
3
- Persistent, type-safe state management for React.
3
+ AI-friendly, persistent, type-safe state for React.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/react-mnemonic.svg)](https://www.npmjs.com/package/react-mnemonic)
6
6
  [![docs](https://img.shields.io/badge/docs-online-0A7EA4.svg)](https://thirtytwobits.github.io/react-mnemonic/)
7
- [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=thirtytwobits_react-mnemonic&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=thirtytwobits_react-mnemonic)
8
- [![dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](https://www.npmjs.com/package/react-mnemonic)
9
- [![license](https://img.shields.io/npm/l/react-mnemonic.svg)](./LICENSE.md)
10
- [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
7
+ [![license](https://img.shields.io/npm/l/react-mnemonic.svg)](https://github.com/thirtytwobits/react-mnemonic/blob/main/LICENSE.md)
11
8
 
12
- **react-mnemonic** gives your React components persistent memory. Values survive
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
- ```bash
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 inside it.
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
- The counter value persists in `localStorage` under the key `my-app.count` and
104
- survives full page reloads.
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
- Persist only the durable slice of your app state. `useMnemonicKey` stores
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
- For self-service recovery UX, pair your per-key hooks with
143
- `useMnemonicRecovery` so users can clear stale filters, reset broken settings,
144
- or fully wipe a namespace without opening DevTools. See the
145
- [Reset and Recovery guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/reset-and-recovery)
146
- for soft-reset and hard-reset recipes.
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
- If a field must stay cleared across reloads, model it as nullable and persist
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
- ## Canonical key definitions
155
-
156
- When the same persisted key appears in more than one component, define it once
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
- ### `StorageLike`
67
+ ## AI resources
401
68
 
402
- The interface your custom storage backend must satisfy.
403
-
404
- ```ts
405
- interface StorageLike {
406
- getItem(key: string): string | null;
407
- setItem(key: string, value: string): void;
408
- removeItem(key: string): void;
409
- key?(index: number): string | null;
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
- ## Disclaimer
78
+ ## Learn more
791
79
 
792
- THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
793
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
794
- FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. The authors make no guarantees
795
- regarding the reliability, availability, or suitability of this library for any
796
- particular use case. See the [MIT License](./LICENSE.md) for full terms.
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](./LICENSE.md) -- Copyright Scott Dixon
93
+ [MIT](https://github.com/thirtytwobits/react-mnemonic/blob/main/LICENSE.md)