Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
React Mnemonic
https://github.com/thirtytwobits/react-mnemonic
Admin
React Mnemonic is an AI-friendly, type-safe, persistent state management library for React that
...
Tokens:
37,752
Snippets:
221
Trust Score:
7.5
Update:
2 months ago
Context
Skills
Chat
Benchmark
96.9
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# React Mnemonic React Mnemonic is an AI-friendly, persistent, type-safe state management library for React applications. It provides a `useState`-like API through the `useMnemonicKey` hook that persists values to localStorage (or custom storage backends), surviving page reloads and optionally syncing across browser tabs. The library is built with SSR-safety in mind and requires zero runtime dependencies. The library offers three entrypoints: `react-mnemonic/core` for lean persisted-state functionality, `react-mnemonic/schema` for JSON Schema validation, versioning, and migrations, and the root `react-mnemonic` for full backward compatibility. Values are stored in versioned envelopes that enable automatic schema migrations when data shapes evolve between app versions. ## MnemonicProvider The `MnemonicProvider` component creates a namespaced storage scope for child components. All keys written by hooks inside the provider are prefixed with the namespace to prevent collisions between different parts of your application. ```tsx import { MnemonicProvider, useMnemonicKey } from "react-mnemonic/core"; function Counter() { const { value: count, set } = useMnemonicKey("count", { defaultValue: 0, }); return ( <div> <p>Count: {count}</p> <button onClick={() => set((c) => c + 1)}>Increment</button> </div> ); } export default function App() { return ( <MnemonicProvider namespace="my-app"> <Counter /> </MnemonicProvider> ); } // The counter persists in localStorage as "my-app.count" // and survives full page reloads ``` ## useMnemonicKey The `useMnemonicKey` hook provides a `useState`-like API for persistent state. It returns an object with `value`, `set`, `reset`, and `remove` functions for managing persisted data. ```tsx import { useMnemonicKey } from "react-mnemonic/core"; interface UserPreferences { theme: "light" | "dark"; fontSize: number; notifications: boolean; } function SettingsPanel() { const { value: prefs, set, reset, remove } = useMnemonicKey<UserPreferences>("preferences", { defaultValue: { theme: "light", fontSize: 14, notifications: true, }, onChange: (newPrefs, oldPrefs) => { console.log("Preferences changed from", oldPrefs, "to", newPrefs); }, onMount: (initialPrefs) => { document.body.className = initialPrefs.theme; }, }); return ( <div> <select value={prefs.theme} onChange={(e) => set({ ...prefs, theme: e.target.value as "light" | "dark" })} > <option value="light">Light</option> <option value="dark">Dark</option> </select> <button onClick={() => reset()}>Reset to Defaults</button> <button onClick={() => remove()}>Clear All Preferences</button> </div> ); } ``` ## defineMnemonicKey The `defineMnemonicKey` function creates reusable, importable key descriptors that package the storage key and its options into a stable object. This helps keep persistence behavior explicit and consistent across components. ```tsx import { defineMnemonicKey, useMnemonicKey, MnemonicProvider } from "react-mnemonic/core"; // Define once at module scope const themeKey = defineMnemonicKey("theme", { defaultValue: "light" as "light" | "dark" | "system", listenCrossTab: true, }); const sidebarKey = defineMnemonicKey("sidebar-collapsed", { defaultValue: false, }); // Reuse across components function Header() { const { value: theme, set } = useMnemonicKey(themeKey); return ( <button onClick={() => set(theme === "dark" ? "light" : "dark")}> Toggle Theme: {theme} </button> ); } function Sidebar() { const { value: collapsed, set } = useMnemonicKey(sidebarKey); return ( <aside style={{ width: collapsed ? 50 : 250 }}> <button onClick={() => set(!collapsed)}> {collapsed ? "Expand" : "Collapse"} </button> </aside> ); } ``` ## Cross-Tab Synchronization Enable cross-tab synchronization with the `listenCrossTab` option so that changes in one browser tab are reflected in all other tabs reading the same key. ```tsx import { useMnemonicKey } from "react-mnemonic/core"; function ThemeSwitcher() { const { value: theme, set } = useMnemonicKey<"light" | "dark">("theme", { defaultValue: "light", listenCrossTab: true, onChange: (newTheme) => { // Update document when theme changes (including from other tabs) document.documentElement.setAttribute("data-theme", newTheme); }, }); return ( <select value={theme} onChange={(e) => set(e.target.value as "light" | "dark")}> <option value="light">Light</option> <option value="dark">Dark</option> </select> ); } // Changes in one tab automatically sync to all other tabs ``` ## Custom Storage Backends Provide a custom synchronous storage backend by implementing the `StorageLike` interface. This enables use of sessionStorage, in-memory storage for testing, or custom adapters. ```tsx import { MnemonicProvider, useMnemonicKey, type StorageLike } from "react-mnemonic/core"; // In-memory storage for testing const mockStorage: StorageLike = { items: new Map<string, string>(), getItem(key) { return this.items.get(key) ?? null; }, setItem(key, value) { this.items.set(key, value); }, removeItem(key) { this.items.delete(key); }, get length() { return this.items.size; }, key(index) { return Array.from(this.items.keys())[index] ?? null; }, }; // Custom storage with BroadcastChannel for cross-tab sync const idbCacheStorage: StorageLike = { cache: new Map<string, string>(), getItem(key) { return this.cache.get(key) ?? null; }, setItem(key, value) { this.cache.set(key, value); // Notify other tabs new BroadcastChannel("app-sync").postMessage({ keys: [key] }); }, removeItem(key) { this.cache.delete(key); }, onExternalChange: (callback) => { const bc = new BroadcastChannel("app-sync"); bc.onmessage = (e) => callback(e.data.keys); return () => bc.close(); }, }; function App() { return ( <MnemonicProvider namespace="test-app" storage={mockStorage}> <MyComponents /> </MnemonicProvider> ); } ``` ## Custom Codecs Create custom codecs using `createCodec` for types that need special serialization, such as `Date`, `Set`, or `Map`. Using a custom codec bypasses JSON Schema validation. ```tsx import { createCodec, useMnemonicKey, CodecError } from "react-mnemonic/core"; // Date codec const DateCodec = createCodec<Date>( (date) => date.toISOString(), (str) => new Date(str) ); // Set codec const StringSetCodec = createCodec<Set<string>>( (set) => JSON.stringify([...set]), (str) => new Set(JSON.parse(str)) ); // Map codec const MapCodec = createCodec<Map<string, number>>( (map) => JSON.stringify([...map.entries()]), (str) => new Map(JSON.parse(str)) ); function LastVisitTracker() { const { value: lastVisit, set } = useMnemonicKey<Date>("lastVisit", { defaultValue: new Date(), codec: DateCodec, }); return ( <div> <p>Last visit: {lastVisit.toLocaleString()}</p> <button onClick={() => set(new Date())}>Update</button> </div> ); } function TagManager() { const { value: tags, set } = useMnemonicKey<Set<string>>("tags", { defaultValue: (error) => { if (error instanceof CodecError) { console.warn("Failed to decode tags:", error.message); } return new Set<string>(); }, codec: StringSetCodec, }); const addTag = (tag: string) => set(new Set([...tags, tag])); const removeTag = (tag: string) => { const newTags = new Set(tags); newTags.delete(tag); set(newTags); }; return ( <div> {[...tags].map((tag) => ( <span key={tag}> {tag} <button onClick={() => removeTag(tag)}>x</button> </span> ))} </div> ); } ``` ## Server-Side Rendering (SSR) The library is SSR-safe by default. Use `ssr.serverValue` for deterministic server placeholders and `ssr.hydration` to control when storage is read. ```tsx import { MnemonicProvider, useMnemonicKey } from "react-mnemonic"; // Next.js example with server placeholder // app/theme-toggle.tsx "use client"; function ThemeToggle({ serverTheme }: { serverTheme: "light" | "dark" | "system" }) { const { value, set } = useMnemonicKey("theme", { defaultValue: "light" as "light" | "dark" | "system", ssr: { serverValue: serverTheme, // Use server-known value during SSR hydration: "immediate", // Read storage immediately after hydration }, }); return <button onClick={() => set(value === "dark" ? "light" : "dark")}>{value}</button>; } // Delay storage reads until after mount function SidebarState() { const { value: collapsed } = useMnemonicKey("sidebar", { defaultValue: false, ssr: { hydration: "client-only", // Don't read storage until after mount }, }); return <aside style={{ width: collapsed ? 50 : 250 }}>...</aside>; } // Provider-level SSR defaults function App() { return ( <MnemonicProvider namespace="app" ssr={{ hydration: "client-only" }} > <MyComponents /> </MnemonicProvider> ); } ``` ## Schema Registry and Validation Use `createSchemaRegistry` with JSON Schema definitions for runtime validation. Schema-managed keys store JSON values directly and are validated against their registered schemas. ```tsx import { createSchemaRegistry, MnemonicProvider, useMnemonicKey, type KeySchema, } from "react-mnemonic/schema"; const profileSchemaV1: KeySchema = { key: "profile", version: 1, schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, }, required: ["name", "email"], }, }; const settingsSchema: KeySchema = { key: "settings", version: 1, schema: { type: "object", properties: { theme: { enum: ["light", "dark", "system"] }, fontSize: { type: "integer", minimum: 10, maximum: 24 }, notifications: { type: "boolean" }, }, required: ["theme"], }, }; const registry = createSchemaRegistry({ schemas: [profileSchemaV1, settingsSchema], migrations: [], }); function App() { return ( <MnemonicProvider namespace="app" schemaMode="default" schemaRegistry={registry} > <ProfileEditor /> </MnemonicProvider> ); } // Values are automatically validated against the schema function ProfileEditor() { const { value, set } = useMnemonicKey("profile", { defaultValue: { name: "", email: "" }, }); return ( <form> <input value={value.name} onChange={(e) => set({ ...value, name: e.target.value })} /> <input value={value.email} onChange={(e) => set({ ...value, email: e.target.value })} /> </form> ); } ``` ## Typed Schema Builder The `mnemonicSchema` builder creates strongly-typed schemas where TypeScript types are inferred from the schema definition, providing a single source of truth. ```tsx import { mnemonicSchema, defineKeySchema, defineMnemonicKey, createSchemaRegistry, useMnemonicKey, MnemonicProvider, } from "react-mnemonic/schema"; // Build typed schema - TypeScript type is inferred automatically const userSettingsSchema = mnemonicSchema.object({ theme: mnemonicSchema.enum(["light", "dark", "system"]), fontSize: mnemonicSchema.integer({ minimum: 10, maximum: 24 }), locale: mnemonicSchema.string(), notifications: mnemonicSchema.object({ email: mnemonicSchema.boolean(), push: mnemonicSchema.boolean(), frequency: mnemonicSchema.optional(mnemonicSchema.enum(["daily", "weekly", "never"])), }), }); // Inferred type: { theme: "light" | "dark" | "system"; fontSize: number; ... } // Create versioned key schema const settingsKeySchema = defineKeySchema("settings", 1, userSettingsSchema); // Create reusable key descriptor bound to the schema const settingsKey = defineMnemonicKey(settingsKeySchema, { defaultValue: { theme: "system", fontSize: 14, locale: "en-US", notifications: { email: true, push: false }, }, }); const registry = createSchemaRegistry({ schemas: [settingsKeySchema], }); function SettingsPage() { // Type-safe: value is fully typed from the schema const { value, set } = useMnemonicKey(settingsKey); return ( <div> <select value={value.theme} onChange={(e) => set({ ...value, theme: e.target.value as "light" | "dark" | "system" })} > <option value="light">Light</option> <option value="dark">Dark</option> <option value="system">System</option> </select> <input type="number" value={value.fontSize} min={10} max={24} onChange={(e) => set({ ...value, fontSize: parseInt(e.target.value) })} /> </div> ); } ``` ## Schema Migrations Define migrations to automatically transform data when schema versions change. Migrations run once when reading old data, then persist the upgraded value. ```tsx import { createSchemaRegistry, defineKeySchema, defineMigration, mnemonicSchema, MnemonicProvider, useMnemonicKey, type MigrationRule, } from "react-mnemonic/schema"; // Version 1 schema const userSchemaV1 = defineKeySchema("user", 1, mnemonicSchema.object({ name: mnemonicSchema.string(), })); // Version 2 schema (split name into firstName/lastName) const userSchemaV2 = defineKeySchema("user", 2, mnemonicSchema.object({ firstName: mnemonicSchema.string(), lastName: mnemonicSchema.string(), migratedAt: mnemonicSchema.string(), })); // Type-safe migration from v1 to v2 const userV1ToV2 = defineMigration(userSchemaV1, userSchemaV2, (v1) => { const parts = v1.name.split(" "); return { firstName: parts[0] || "", lastName: parts.slice(1).join(" ") || "", migratedAt: new Date().toISOString(), }; }); // Write-time normalizer (same version): runs on every write const trimUserV2: MigrationRule = { key: "user", fromVersion: 2, toVersion: 2, migrate: (value) => { const user = value as { firstName: string; lastName: string; migratedAt: string }; return { ...user, firstName: user.firstName.trim(), lastName: user.lastName.trim(), }; }, }; const registry = createSchemaRegistry({ schemas: [userSchemaV1, userSchemaV2], migrations: [userV1ToV2, trimUserV2], }); function App() { return ( <MnemonicProvider namespace="app" schemaRegistry={registry} schemaMode="default"> <UserProfile /> </MnemonicProvider> ); } // When reading a v1 user, it's automatically migrated to v2 function UserProfile() { const { value, set } = useMnemonicKey("user", { defaultValue: { firstName: "", lastName: "", migratedAt: "" }, }); return ( <div> <input value={value.firstName} onChange={(e) => set({ ...value, firstName: e.target.value })} placeholder="First name" /> <input value={value.lastName} onChange={(e) => set({ ...value, lastName: e.target.value })} placeholder="Last name" /> </div> ); } ``` ## Reconciliation Use the `reconcile` option for conditional, read-time adjustments that should apply after decode and migration, without requiring a full schema version bump. ```tsx import { useMnemonicKey } from "react-mnemonic/schema"; interface AppSettings { theme: "light" | "dark"; marketingOptIn: boolean; newFeatureEnabled: boolean; } function SettingsManager() { const { value, set } = useMnemonicKey<AppSettings>("app-settings", { defaultValue: { theme: "light", marketingOptIn: false, newFeatureEnabled: true, }, reconcile: (persisted, { persistedVersion, latestVersion }) => ({ ...persisted, // Force new feature on for users who haven't seen it yet newFeatureEnabled: persistedVersion < (latestVersion ?? 1) ? true : persisted.newFeatureEnabled, // Apply new default for marketing opt-in on older data marketingOptIn: persistedVersion < 2 ? true : persisted.marketingOptIn, }), }); return ( <div> <label> <input type="checkbox" checked={value.newFeatureEnabled} onChange={(e) => set({ ...value, newFeatureEnabled: e.target.checked })} /> Enable new feature </label> </div> ); } ``` ## Recovery Helpers Use `useMnemonicRecovery` for namespace-scoped recovery actions like clearing all data, clearing specific keys, or clearing keys matching a pattern. ```tsx import { useMnemonicRecovery, MnemonicProvider } from "react-mnemonic/schema"; function RecoveryPanel() { const { namespace, canEnumerateKeys, listKeys, clearAll, clearKeys, clearMatching, } = useMnemonicRecovery({ onRecover: (event) => { console.log(`Recovery action "${event.action}" cleared:`, event.clearedKeys); // Send to analytics, show toast, etc. }, }); const handleClearFilters = () => { // Clear all keys starting with "filter-" clearMatching((key) => key.startsWith("filter-")); }; const handleFactoryReset = () => { if (confirm("Clear all app data? This cannot be undone.")) { clearAll(); } }; const handleClearSpecific = () => { clearKeys(["user-preferences", "cached-data", "session-state"]); }; return ( <div> <p>Namespace: {namespace}</p> <p>Keys: {listKeys().join(", ") || "(none)"}</p> <button onClick={handleClearFilters}>Clear Saved Filters</button> <button onClick={handleClearSpecific}>Clear Specific Keys</button> <button onClick={handleFactoryReset}>Factory Reset</button> </div> ); } ``` ## DevTools Integration Enable DevTools for browser console debugging. Access providers through `window.__REACT_MNEMONIC_DEVTOOLS__` to inspect, modify, and clear persisted state. ```tsx import { MnemonicProvider } from "react-mnemonic"; function App() { return ( <MnemonicProvider namespace="my-app" enableDevTools={process.env.NODE_ENV === "development"} > <MyComponents /> </MnemonicProvider> ); } // In browser console: // const dt = window.__REACT_MNEMONIC_DEVTOOLS__ // dt.list() // List all registered providers // const provider = dt.resolve("my-app") // provider.dump() // Show all keys and values // provider.get("theme") // Read decoded value // provider.set("theme", "dark") // Write value // provider.remove("theme") // Delete key // provider.clear() // Clear all keys in namespace // provider.keys() // List all keys ``` ## JSON Schema Validation Use `validateJsonSchema` and `compileSchema` for standalone JSON Schema validation. The library supports a subset of JSON Schema sufficient for localStorage state management. ```tsx import { validateJsonSchema, compileSchema, type JsonSchema } from "react-mnemonic/schema"; const userSchema: JsonSchema = { type: "object", properties: { name: { type: "string", minLength: 1, maxLength: 100 }, age: { type: "integer", minimum: 0, maximum: 150 }, email: { type: "string" }, roles: { type: "array", items: { enum: ["admin", "user", "guest"] }, minItems: 1, }, }, required: ["name", "email"], additionalProperties: false, }; // One-off validation const errors = validateJsonSchema( { name: "Alice", email: "alice@example.com", age: 30, roles: ["user"] }, userSchema ); if (errors.length > 0) { errors.forEach((err) => { console.error(`${err.path}: ${err.message} (${err.keyword})`); }); } else { console.log("Valid!"); } // Pre-compiled validator for repeated use (cached automatically) const validate = compileSchema(userSchema); const result = validate({ name: "", email: "test@example.com" }); // result: [{ path: "/name", message: "String length 0 is less than minLength 1", keyword: "minLength" }] ``` ## Summary React Mnemonic is ideal for applications that need persistent client-side state with type safety and SSR compatibility. Common use cases include user preferences (themes, layouts, sidebar states), form drafts, shopping cart data, feature flags, and any UI state that should survive page reloads. The library excels when you need schema validation and automatic migrations to safely evolve data shapes over time. Integration patterns typically involve wrapping your app in a `MnemonicProvider` at the root level, defining key descriptors at module scope for reuse across components, and optionally setting up a schema registry for validation and migrations. For testing, provide a mock `StorageLike` implementation. For SSR frameworks like Next.js or Remix, use the `ssr` options to control server placeholders and hydration timing. The DevTools integration helps during development for inspecting and debugging persisted state directly from the browser console.