Persistence
The persist option
persist is an option on createAtom. It accepts:
| Value | Effect |
|---|---|
true | Use the built-in localStorageAdapter (JSON encode/decode, client-only) |
false / omitted | No persistence |
PersistAdapter object | Use your custom adapter (sync or async) |
Built-in localStorage (client-only)
import { createAtom } from "@mongez/atom";
const themeAtom = createAtom({ key: "ui.theme", default: "light" as "light" | "dark", persist: true,});
// On first load: themeAtom.value === "light" (default)// After user picks "dark": themeAtom.value === "dark", written to localStorage["ui.theme"]// On next page load: themeAtom.value === "dark" (restored silently)themeAtom.update("dark");The atom key is used directly as the localStorage key. JSON.stringify/parse is applied automatically.
Server note: The built-in adapter checks typeof window === "undefined" and no-ops silently. On the server the atom always starts from default. For SSR-safe persistence, use a custom cookie adapter (see below).
Lifecycle of persisted state
- Creation — the adapter’s
get(key)is called. If a value is present, it is applied viasilentUpdate(noupdateevent fires, so React subscribers don’t trigger an extra render on mount). - Every update — the adapter’s
set(key, value)is called synchronously insideonChange. Async adapters are awaited internally; errors are swallowed so a storage failure never breaks the update flow. - reset() — the adapter’s
remove(key)is called, clearing the persisted entry. The atom goes back to itsdefault.
Custom adapter — shape
import { type PersistAdapter } from "@mongez/atom";
// Methods may return plain values (sync) or Promises (async).const myAdapter: PersistAdapter = { get(key: string): unknown | undefined | Promise<unknown | undefined> { /* ... */ }, set(key: string, value: unknown): void | Promise<void> { /* ... */ }, remove(key: string): void | Promise<void> { /* ... */ },};Cookie adapter (SSR-safe example)
For Next.js or similar SSR frameworks, use a cookie-based adapter so the server can read persisted values during rendering:
import { type PersistAdapter } from "@mongez/atom";
// Works on both client (document.cookie) and server (req.cookies injected via closure).function makeCookieAdapter( getServerCookies?: () => Record<string, string>): PersistAdapter { return { get(key) { if (typeof document !== "undefined") { const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]*)`)); return match ? JSON.parse(decodeURIComponent(match[1])) : undefined; } // Server-side: use injected request cookies const cookies = getServerCookies?.() ?? {}; const raw = cookies[key]; return raw !== undefined ? JSON.parse(raw) : undefined; }, set(key, value) { if (typeof document === "undefined") return; document.cookie = `${key}=${encodeURIComponent(JSON.stringify(value))};path=/;max-age=31536000`; }, remove(key) { if (typeof document === "undefined") return; document.cookie = `${key}=;path=/;max-age=0`; }, };}
const themeAtom = createAtom({ key: "ui.theme", default: "light", persist: makeCookieAdapter(),});IndexedDB adapter (async example)
import { type PersistAdapter } from "@mongez/atom";
const idbAdapter: PersistAdapter = { async get(key) { const db = await openDB(); // your idb helper return db.get("atoms", key); }, async set(key, value) { const db = await openDB(); await db.put("atoms", value, key); }, async remove(key) { const db = await openDB(); await db.delete("atoms", key); },};
const heavyAtom = createAtom({ key: "heavy.data", default: {}, persist: idbAdapter,});Per-atom adapter (different stores for different atoms)
You can pass a different adapter per atom — no global configuration needed:
const prefs = createAtom({ key: "prefs", default: { fontSize: 14 }, persist: localStorageAdapter, // fine for user prefs});
const session = createAtom({ key: "session", default: { token: "" }, persist: sessionStorageAdapter, // your custom adapter backed by sessionStorage});Accessing the built-in adapter directly
import { localStorageAdapter } from "@mongez/atom";
// Use as-is:const atom = createAtom({ key: "x", default: 0, persist: localStorageAdapter });
// Or extend it:const prefixedAdapter: PersistAdapter = { get: key => localStorageAdapter.get(`myapp:${key}`), set: (key, v) => localStorageAdapter.set(`myapp:${key}`, v), remove: key => localStorageAdapter.remove(`myapp:${key}`),};Key pitfalls
persist: trueis client-only. On Node/SSR,windowis undefined and the adapter silently no-ops. The atom always starts fromdefaulton the server. Use a cookie adapter for SSR.- Async adapter, async hydration. When
get()returns aPromise, the atom starts atdefaultand switches to the stored value when the promise resolves. In React, this causes a one-render delay. For SSR, sync adapters (cookies) avoid this flash. reset()removes the storage entry. The next session starts fromdefaultagain. This is intentional — a reset means “clear persisted state”.- The atom key is the storage key. If you rename an atom’s key, old persisted data under the previous key becomes orphaned. Clean up manually if needed.
- Adapter errors are swallowed.
QuotaExceededError, private-mode blocks, or any sync throw insideset/removeis caught and ignored so the atom update still succeeds. Monitor storage errors separately if needed. beforeUpdatestill applies after restore. The value loaded from the adapter passes throughbeforeUpdateviasilentUpdate. Make sure yourbeforeUpdatehandles the persisted shape correctly.