react-mnemonic 0.1.1-alpha.0 → 1.0.0-beta.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 +171 -85
- package/dist/index.cjs +615 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +578 -10
- package/dist/index.d.ts +578 -10
- package/dist/index.js +610 -127
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,16 +7,13 @@ Persistent, type-safe state management for React.
|
|
|
7
7
|
[](./LICENSE.md)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
-
> **⚠️ Alpha Software** — This library is in active development. APIs may
|
|
11
|
-
> change between releases without prior notice. Use in production at your own
|
|
12
|
-
> risk.
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
10
|
**react-mnemonic** gives your React components persistent memory. Values survive
|
|
17
11
|
page refreshes, synchronize across tabs, and stay type-safe end-to-end -- all
|
|
18
12
|
through a single hook that works like `useState`.
|
|
19
13
|
|
|
14
|
+
`1.0.0` is currently being shipped as a beta on the npm `beta` dist-tag while
|
|
15
|
+
the release candidate hardening work finishes.
|
|
16
|
+
|
|
20
17
|
## Features
|
|
21
18
|
|
|
22
19
|
- **`useState`-like API** -- `useMnemonicKey` returns `{ value, set, reset, remove }`
|
|
@@ -25,6 +22,9 @@ through a single hook that works like `useState`.
|
|
|
25
22
|
- **Cross-tab sync** -- opt-in `listenCrossTab` uses the browser `storage` event
|
|
26
23
|
- **Pluggable storage** -- bring your own backend via the `StorageLike` interface (IndexedDB, sessionStorage, etc.)
|
|
27
24
|
- **Schema versioning and migration** -- upgrade stored data with versioned schemas and migration rules
|
|
25
|
+
- **Structural migration helpers** -- optional tree utilities for idempotent insert/rename/dedupe migration steps
|
|
26
|
+
- **Read-time reconciliation** -- selectively enforce new defaults on persisted values without clearing the whole key
|
|
27
|
+
- **Recovery helpers** -- build user-facing soft reset and hard reset flows with namespace-scoped clear helpers
|
|
28
28
|
- **Write-time normalization** -- migrations where `fromVersion === toVersion` run on every write
|
|
29
29
|
- **Lifecycle callbacks** -- `onMount` and `onChange` hooks
|
|
30
30
|
- **DevTools** -- inspect and mutate state from the browser console
|
|
@@ -34,20 +34,21 @@ through a single hook that works like `useState`.
|
|
|
34
34
|
## Installation
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npm install react-mnemonic
|
|
37
|
+
npm install react-mnemonic@beta
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
yarn add react-mnemonic
|
|
41
|
+
yarn add react-mnemonic@beta
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
pnpm add react-mnemonic
|
|
45
|
+
pnpm add react-mnemonic@beta
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
### Peer dependencies
|
|
49
49
|
|
|
50
|
-
React 18 or later is required.
|
|
50
|
+
React 18 or later is required. CI verifies packaged-consumer installs against
|
|
51
|
+
React 18 and React 19.
|
|
51
52
|
|
|
52
53
|
```json
|
|
53
54
|
{
|
|
@@ -90,6 +91,25 @@ export default function App() {
|
|
|
90
91
|
The counter value persists in `localStorage` under the key `my-app.count` and
|
|
91
92
|
survives full page reloads.
|
|
92
93
|
|
|
94
|
+
Persist only the durable slice of your app state. `useMnemonicKey` stores
|
|
95
|
+
whatever you pass to `set`, so keep transient UI state like loading flags,
|
|
96
|
+
hover state, and draft search text in plain React state unless you explicitly
|
|
97
|
+
want them to rehydrate after reload. See the
|
|
98
|
+
[Persisted vs Ephemeral State guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/persisted-vs-ephemeral-state)
|
|
99
|
+
for patterns and an interactive example.
|
|
100
|
+
|
|
101
|
+
For self-service recovery UX, pair your per-key hooks with
|
|
102
|
+
`useMnemonicRecovery` so users can clear stale filters, reset broken settings,
|
|
103
|
+
or fully wipe a namespace without opening DevTools. See the
|
|
104
|
+
[Reset and Recovery guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/reset-and-recovery)
|
|
105
|
+
for soft-reset and hard-reset recipes.
|
|
106
|
+
|
|
107
|
+
If a field must stay cleared across reloads, model it as nullable and persist
|
|
108
|
+
`null` explicitly. `remove()` deletes the key and falls back to `defaultValue`,
|
|
109
|
+
while `reset()` writes the default again. See the
|
|
110
|
+
[Clearable Persisted Values guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/clearable-persisted-values)
|
|
111
|
+
for the canonical nullable pattern.
|
|
112
|
+
|
|
93
113
|
## API
|
|
94
114
|
|
|
95
115
|
### `<MnemonicProvider>`
|
|
@@ -125,16 +145,48 @@ const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
|
|
|
125
145
|
| `reset` | `() => void` | Reset to `defaultValue` and persist it |
|
|
126
146
|
| `remove` | `() => void` | Delete the key from storage entirely |
|
|
127
147
|
|
|
148
|
+
For clearable fields, remember the semantic split:
|
|
149
|
+
|
|
150
|
+
- `set(null)` persists a cleared value and stays cleared after reload
|
|
151
|
+
- `remove()` deletes the key, so the next read falls back to `defaultValue`
|
|
152
|
+
- `reset()` persists `defaultValue`
|
|
153
|
+
|
|
128
154
|
#### Options
|
|
129
155
|
|
|
130
|
-
| Option | Type | Default | Description
|
|
131
|
-
| ---------------- | ------------------------------------------------- | ----------- |
|
|
132
|
-
| `defaultValue` | `T \| ((error?: CodecError \| SchemaError) => T)` | _required_ | Fallback value or error-aware factory
|
|
133
|
-
| `codec` | `Codec<T>` | `JSONCodec` | Encode/decode strategy (bypasses schema validation)
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
156
|
+
| Option | Type | Default | Description |
|
|
157
|
+
| ---------------- | ------------------------------------------------- | ----------- | ------------------------------------------------------------- |
|
|
158
|
+
| `defaultValue` | `T \| ((error?: CodecError \| SchemaError) => T)` | _required_ | Fallback value or error-aware factory |
|
|
159
|
+
| `codec` | `Codec<T>` | `JSONCodec` | Encode/decode strategy (bypasses schema validation) |
|
|
160
|
+
| `reconcile` | `(value: T, context: ReconcileContext) => T` | -- | Adjust a persisted value after read and persist if it changes |
|
|
161
|
+
| `onMount` | `(value: T) => void` | -- | Called once with the initial value |
|
|
162
|
+
| `onChange` | `(value: T, prev: T) => void` | -- | Called on every value change |
|
|
163
|
+
| `listenCrossTab` | `boolean` | `false` | Sync via the browser `storage` event |
|
|
164
|
+
| `schema` | `{ version?: number }` | -- | Pin writes to a specific schema version |
|
|
165
|
+
|
|
166
|
+
### `useMnemonicRecovery(options)`
|
|
167
|
+
|
|
168
|
+
Hook for namespace-scoped recovery actions such as "clear saved filters" or
|
|
169
|
+
"reset all persisted app data".
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const { namespace, canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching } = useMnemonicRecovery({
|
|
173
|
+
onRecover: (event) => console.log(event.action, event.clearedKeys),
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Return | Type | Description |
|
|
178
|
+
| ------------------ | --------------------------------------------------- | ----------------------------------------------------- |
|
|
179
|
+
| `namespace` | `string` | Current provider namespace |
|
|
180
|
+
| `canEnumerateKeys` | `boolean` | Whether the storage backend can list namespace keys |
|
|
181
|
+
| `listKeys` | `() => string[]` | List visible unprefixed keys in the current namespace |
|
|
182
|
+
| `clearAll` | `() => string[]` | Clear every key in the namespace |
|
|
183
|
+
| `clearKeys` | `(keys: readonly string[]) => string[]` | Clear an explicit set of unprefixed keys |
|
|
184
|
+
| `clearMatching` | `(predicate: (key: string) => boolean) => string[]` | Clear keys whose names match a predicate |
|
|
185
|
+
|
|
186
|
+
`clearAll()` and `clearMatching()` require an enumerable storage backend such
|
|
187
|
+
as `localStorage` or `sessionStorage`. If your custom storage does not support
|
|
188
|
+
`length` and `key(index)`, use `clearKeys([...])` with the explicit durable-key
|
|
189
|
+
list your app owns.
|
|
138
190
|
|
|
139
191
|
### Codecs
|
|
140
192
|
|
|
@@ -172,6 +224,8 @@ interface StorageLike {
|
|
|
172
224
|
`onExternalChange` enables cross-tab sync for non-localStorage backends (e.g.
|
|
173
225
|
IndexedDB over `BroadcastChannel`). The library handles all error cases
|
|
174
226
|
internally -- see the `StorageLike` JSDoc for the full error-handling contract.
|
|
227
|
+
Namespace-wide recovery helpers can only enumerate keys when the backend also
|
|
228
|
+
implements `length` and `key(index)`.
|
|
175
229
|
|
|
176
230
|
### `validateJsonSchema(schema, value)`
|
|
177
231
|
|
|
@@ -318,84 +372,109 @@ const normalizer: MigrationRule = {
|
|
|
318
372
|
};
|
|
319
373
|
```
|
|
320
374
|
|
|
375
|
+
### Reconciliation
|
|
376
|
+
|
|
377
|
+
Use `reconcile` when you want to keep persisted data but selectively enforce
|
|
378
|
+
new application defaults after the value has been decoded and any read-time
|
|
379
|
+
migrations have already run.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const { value } = useMnemonicKey("preferences", {
|
|
383
|
+
defaultValue: { theme: "dark", density: "comfortable", accents: true },
|
|
384
|
+
reconcile: (persisted, { persistedVersion }) => ({
|
|
385
|
+
...persisted,
|
|
386
|
+
accents: persistedVersion === 0 ? true : persisted.accents,
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Use a schema migration when the stored shape must move from one explicit version
|
|
392
|
+
to another. Use `reconcile` for conditional, field-level policy changes such as
|
|
393
|
+
rolling out a new default while preserving the rest of a user's stored data.
|
|
394
|
+
|
|
395
|
+
### Structural migration helpers
|
|
396
|
+
|
|
397
|
+
For layout-like data that already uses `id` and `children`, Mnemonic ships
|
|
398
|
+
optional pure helpers for common idempotent migration steps:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { insertChildIfMissing, renameNode, dedupeChildrenBy } from "react-mnemonic";
|
|
402
|
+
|
|
403
|
+
const migrated = dedupeChildrenBy(
|
|
404
|
+
renameNode(insertChildIfMissing(layout, "sidebar", { id: "search", title: "Search" }), "prefs", "preferences"),
|
|
405
|
+
(node) => node.id,
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Use these inside your `MigrationRule.migrate` functions when you want repeatable
|
|
410
|
+
tree edits without hand-writing the same traversal logic each time. See the
|
|
411
|
+
[Schema Migration guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/schema-migration)
|
|
412
|
+
for a cookbook example and custom adapter usage.
|
|
413
|
+
|
|
321
414
|
### Example schema registry
|
|
322
415
|
|
|
323
416
|
A schema registry stores versioned schemas for each key, and resolves migration
|
|
324
|
-
paths to upgrade stored data.
|
|
325
|
-
|
|
417
|
+
paths to upgrade stored data. For the common immutable case, use
|
|
418
|
+
`createSchemaRegistry(...)` instead of hand-rolling the indexing boilerplate.
|
|
326
419
|
|
|
327
420
|
```tsx
|
|
328
421
|
import {
|
|
422
|
+
createSchemaRegistry,
|
|
329
423
|
MnemonicProvider,
|
|
330
424
|
useMnemonicKey,
|
|
331
|
-
type SchemaRegistry,
|
|
332
425
|
type KeySchema,
|
|
333
426
|
type MigrationRule,
|
|
334
427
|
} from "react-mnemonic";
|
|
335
428
|
|
|
336
|
-
const schemas =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
getMigrationPath: (key, fromVersion, toVersion) => {
|
|
346
|
-
const byKey = migrations.filter((rule) => rule.key === key);
|
|
347
|
-
const path: MigrationRule[] = [];
|
|
348
|
-
let cur = fromVersion;
|
|
349
|
-
while (cur < toVersion) {
|
|
350
|
-
const next = byKey.find((rule) => rule.fromVersion === cur);
|
|
351
|
-
if (!next) return null;
|
|
352
|
-
path.push(next);
|
|
353
|
-
cur = next.toVersion;
|
|
354
|
-
}
|
|
355
|
-
return path;
|
|
356
|
-
},
|
|
357
|
-
getWriteMigration: (key, version) => {
|
|
358
|
-
return migrations.find((r) => r.key === key && r.fromVersion === version && r.toVersion === version);
|
|
359
|
-
},
|
|
360
|
-
registerSchema: (schema) => {
|
|
361
|
-
const id = `${schema.key}:${schema.version}`;
|
|
362
|
-
if (schemas.has(id)) throw new Error(`Schema already registered for ${id}`);
|
|
363
|
-
schemas.set(id, schema);
|
|
429
|
+
const schemas: KeySchema[] = [
|
|
430
|
+
{
|
|
431
|
+
key: "profile",
|
|
432
|
+
version: 1,
|
|
433
|
+
schema: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: { name: { type: "string" }, email: { type: "string" } },
|
|
436
|
+
required: ["name", "email"],
|
|
437
|
+
},
|
|
364
438
|
},
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
439
|
+
{
|
|
440
|
+
key: "profile",
|
|
441
|
+
version: 2,
|
|
442
|
+
schema: {
|
|
443
|
+
type: "object",
|
|
444
|
+
properties: {
|
|
445
|
+
name: { type: "string" },
|
|
446
|
+
email: { type: "string" },
|
|
447
|
+
migratedAt: { type: "string" },
|
|
448
|
+
},
|
|
449
|
+
required: ["name", "email", "migratedAt"],
|
|
450
|
+
},
|
|
374
451
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
migrations
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const migrations: MigrationRule[] = [
|
|
455
|
+
{
|
|
456
|
+
key: "profile",
|
|
457
|
+
fromVersion: 1,
|
|
458
|
+
toVersion: 2,
|
|
459
|
+
migrate: (value) => {
|
|
460
|
+
const v1 = value as { name: string; email: string };
|
|
461
|
+
return { ...v1, migratedAt: new Date().toISOString() };
|
|
462
|
+
},
|
|
384
463
|
},
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
properties: {
|
|
393
|
-
name: { type: "string" },
|
|
394
|
-
email: { type: "string" },
|
|
395
|
-
migratedAt: { type: "string" },
|
|
464
|
+
{
|
|
465
|
+
key: "profile",
|
|
466
|
+
fromVersion: 2,
|
|
467
|
+
toVersion: 2,
|
|
468
|
+
migrate: (value) => {
|
|
469
|
+
const profile = value as { name: string; email: string; migratedAt: string };
|
|
470
|
+
return { ...profile, email: profile.email.trim().toLowerCase() };
|
|
396
471
|
},
|
|
397
|
-
required: ["name", "email", "migratedAt"],
|
|
398
472
|
},
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
const registry = createSchemaRegistry({
|
|
476
|
+
schemas,
|
|
477
|
+
migrations,
|
|
399
478
|
});
|
|
400
479
|
|
|
401
480
|
function ProfileEditor() {
|
|
@@ -410,6 +489,11 @@ function ProfileEditor() {
|
|
|
410
489
|
</MnemonicProvider>;
|
|
411
490
|
```
|
|
412
491
|
|
|
492
|
+
`createSchemaRegistry` validates duplicate schemas and ambiguous migration
|
|
493
|
+
graphs up front. If you need runtime schema registration for
|
|
494
|
+
`schemaMode="autoschema"`, keep a custom mutable `SchemaRegistry`
|
|
495
|
+
implementation.
|
|
496
|
+
|
|
413
497
|
### Registry immutability
|
|
414
498
|
|
|
415
499
|
In `default` and `strict` modes, the schema registry is treated as immutable for
|
|
@@ -452,12 +536,14 @@ Enable the console inspector in development:
|
|
|
452
536
|
Then in the browser console:
|
|
453
537
|
|
|
454
538
|
```js
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
539
|
+
const app = window.__REACT_MNEMONIC_DEVTOOLS__?.resolve("app");
|
|
540
|
+
|
|
541
|
+
app?.dump(); // table of all keys
|
|
542
|
+
app?.get("theme"); // read a decoded value
|
|
543
|
+
app?.set("theme", "dark"); // write
|
|
544
|
+
app?.remove("theme"); // delete
|
|
545
|
+
app?.keys(); // list all keys
|
|
546
|
+
app?.clear(); // remove all keys
|
|
461
547
|
```
|
|
462
548
|
|
|
463
549
|
## TypeScript
|