react-mnemonic 1.0.0-beta.0 → 1.1.0-beta0

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
@@ -3,7 +3,9 @@
3
3
  Persistent, type-safe state management 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
- [![bundle size](https://img.shields.io/bundlephobia/minzip/react-mnemonic)](https://bundlephobia.com/package/react-mnemonic)
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)
7
9
  [![license](https://img.shields.io/npm/l/react-mnemonic.svg)](./LICENSE.md)
8
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
9
11
 
@@ -11,8 +13,16 @@ Persistent, type-safe state management for React.
11
13
  page refreshes, synchronize across tabs, and stay type-safe end-to-end -- all
12
14
  through a single hook that works like `useState`.
13
15
 
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
+ `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`.
16
26
 
17
27
  ## Features
18
28
 
@@ -20,29 +30,31 @@ the release candidate hardening work finishes.
20
30
  - **JSON Schema validation** -- optional schema-based validation using a built-in JSON Schema subset
21
31
  - **Namespace isolation** -- `MnemonicProvider` prefixes every key to prevent collisions
22
32
  - **Cross-tab sync** -- opt-in `listenCrossTab` uses the browser `storage` event
23
- - **Pluggable storage** -- bring your own backend via the `StorageLike` interface (IndexedDB, sessionStorage, etc.)
33
+ - **Pluggable storage** -- use `localStorage`, `sessionStorage`, or a synchronous `StorageLike` facade over custom persistence
24
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
25
36
  - **Structural migration helpers** -- optional tree utilities for idempotent insert/rename/dedupe migration steps
26
37
  - **Read-time reconciliation** -- selectively enforce new defaults on persisted values without clearing the whole key
27
38
  - **Recovery helpers** -- build user-facing soft reset and hard reset flows with namespace-scoped clear helpers
28
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(...)`
29
41
  - **Lifecycle callbacks** -- `onMount` and `onChange` hooks
30
42
  - **DevTools** -- inspect and mutate state from the browser console
31
- - **SSR-safe** -- returns defaults when `window` is unavailable
43
+ - **SSR-safe with explicit controls** -- defaults to `defaultValue` on the server, with optional `ssr.serverValue` and `client-only` hydration
32
44
  - **Tree-shakeable, zero dependencies** -- ships ESM + CJS with full TypeScript declarations
33
45
 
34
46
  ## Installation
35
47
 
36
48
  ```bash
37
- npm install react-mnemonic@beta
49
+ npm install react-mnemonic
38
50
  ```
39
51
 
40
52
  ```bash
41
- yarn add react-mnemonic@beta
53
+ yarn add react-mnemonic
42
54
  ```
43
55
 
44
56
  ```bash
45
- pnpm add react-mnemonic@beta
57
+ pnpm add react-mnemonic
46
58
  ```
47
59
 
48
60
  ### Peer dependencies
@@ -91,6 +103,35 @@ export default function App() {
91
103
  The counter value persists in `localStorage` under the key `my-app.count` and
92
104
  survives full page reloads.
93
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).
134
+
94
135
  Persist only the durable slice of your app state. `useMnemonicKey` stores
95
136
  whatever you pass to `set`, so keep transient UI state like loading flags,
96
137
  hover state, and draft search text in plain React state unless you explicitly
@@ -110,6 +151,125 @@ while `reset()` writes the default again. See the
110
151
  [Clearable Persisted Values guide](https://thirtytwobits.github.io/react-mnemonic/docs/guides/clearable-persisted-values)
111
152
  for the canonical nullable pattern.
112
153
 
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
+
113
273
  ## API
114
274
 
115
275
  ### `<MnemonicProvider>`
@@ -130,11 +290,42 @@ Context provider that scopes storage keys under a namespace.
130
290
 
131
291
  Multiple providers with different namespaces can coexist in the same app.
132
292
 
133
- ### `useMnemonicKey<T>(key, options)`
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)`
134
324
 
135
325
  Hook for reading and writing a single persistent value.
136
326
 
137
327
  ```ts
328
+ const { value, set, reset, remove } = useMnemonicKey(themeKey);
138
329
  const { value, set, reset, remove } = useMnemonicKey<T>(key, options);
139
330
  ```
140
331
 
@@ -221,6 +412,13 @@ interface StorageLike {
221
412
  }
222
413
  ```
223
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
+
224
422
  `onExternalChange` enables cross-tab sync for non-localStorage backends (e.g.
225
423
  IndexedDB over `BroadcastChannel`). The library handles all error cases
226
424
  internally -- see the `StorageLike` JSDoc for the full error-handling contract.
@@ -509,22 +707,40 @@ version and remount the provider.
509
707
  import { MnemonicProvider } from "react-mnemonic";
510
708
  import type { StorageLike } from "react-mnemonic";
511
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
+
512
718
  const idbStorage: StorageLike = {
513
- getItem: (key) => /* read from IndexedDB */,
514
- setItem: (key, value) => /* write to IndexedDB */,
515
- removeItem: (key) => /* delete from IndexedDB */,
516
- onExternalChange: (cb) => {
517
- const bc = new BroadcastChannel("my-app-sync");
518
- bc.onmessage = (e) => cb(e.data.keys);
519
- return () => bc.close();
520
- },
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
+ },
521
733
  };
522
734
 
523
735
  <MnemonicProvider namespace="my-app" storage={idbStorage}>
524
- <App />
525
- </MnemonicProvider>
736
+ <App />
737
+ </MnemonicProvider>;
526
738
  ```
527
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
+
528
744
  ### DevTools
529
745
 
530
746
  Enable the console inspector in development:
@@ -555,6 +771,7 @@ All public types are re-exported from the package root:
555
771
  import type {
556
772
  Codec,
557
773
  StorageLike,
774
+ MnemonicKeyDescriptor,
558
775
  MnemonicProviderOptions,
559
776
  MnemonicProviderProps,
560
777
  UseMnemonicKeyOptions,