SSR & stores
The module-level atom registry is shared per Node process. To keep concurrent SSR requests from leaking state into each other, wrap your tree in <AtomStoreProvider> — each request gets its own scoped clones of every atom that’s read inside it.
<AtomStoreProvider>
import { AtomStoreProvider } from "@mongez/react-atom";
<AtomStoreProvider> <App /></AtomStoreProvider>Props:
type AtomStoreProviderProps = { store?: AtomStore; // bring your own; otherwise auto-created initialAtoms?: Atom<any>[]; // pre-register these initialValues?: Record<string, unknown>; // hydration values keyed by atom key children: React.ReactNode;};- An auto-created store is destroyed a tick after unmount, and not at all if the provider mounts again first, so React StrictMode’s simulated remount keeps the hydrated store.
- On a server (no
document), any atom hook with no provider above it throws, naming the atom key. Wrap the page or layout in<AtomStoreProvider>. - New clones seed from the atom’s
default(orinitialValues), never from the template’s current value. - A
storepassed via props is caller-owned (NOT destroyed by the provider). initialValuesare applied viasilentUpdateso the first render of subscribers sees the hydrated values; no flash.
useAtom(template | key)
Two overloads.
useAtom(template: Atom<V, A>): Atom<V, A> // → store-scoped clone (the template in a browser with no provider; throws on a server with no provider)useAtom(key: string): Atom<V> | undefined // → scoped clone by key, or undefinedThe per-atom hooks (useValue, useState, use, useWatch) already resolve internally — you only need useAtom(template) explicitly when you want to call an action method from an event handler in an SSR-safe way:
function LogoutButton() { const scopedAuth = useAtom(authAtom); return <button onClick={() => scopedAuth.logout()}>Log out</button>;}Resolution is idempotent: useAtom(template).useValue() and template.useValue() use the same clone, and an atom that is already a scoped clone comes back as this store’s clone. The string overload is a legacy escape hatch — prefer the template overload.
Hydration correctness
getServerSnapshotreturns the store’s hydrated value, frozen at the first read, so the first client render matches the server HTML even if effects change the store during hydration.persist: trueonatom()is read after mount (persistHydration: "deferred"), never at creation and never on the server. Scoped clones are not persisted.use("key")andwatchfire for atoms declared withdefault: nullonce they hold an object.
Per-instance state
useLocalAtom({ key, default }) (or atom({ ..., register: false })) makes an atom that stays out of the global registry, ignores the provider and needs none on the server. useLocalAtom suffixes the key with .local.<n> so instances never share events and destroys the atom after unmount.
useAtomStore()
Returns the active AtomStore or null if no provider is mounted. Use for advanced cases: snapshotting on demand, manually hydrating, integrating with a non-React layer.
Snapshot / Hydrate
serializeSnapshot(snapshot: Record<string, unknown>, options?: { replacer, space }): stringserializeStore(store: AtomStore, options?): stringreadHydration(id?: string): Record<string, unknown> | null
<HydrateAtomsScript snapshot={snapshotOrString} id="__mongez_atom_state" // optional; default constant nonce={cspNonce} // optional/>serializeSnapshot/serializeStoreproduce a string safe to embed in<script type="application/json">:</script>,<!--and U+2028/U+2029 are escaped. (<!--matters because it flips the HTML parser into script-data-escaped state, which would make the real closing</script>be swallowed as data.)<HydrateAtomsScript>renders an inline JSON script tag. Place it once per<AtomStoreProvider>you want to hydrate.readHydration()is the client-side companion: parses the JSON out of the script tag. Returnsnullif missing or malformed.
Why hydrate at all?
Without hydration, atoms on the client start from their default value. For data the server already has (current user, feature flags, initial page data), this means the first render shows a loading state — or worse, a different UI than the server rendered — causing a hydration mismatch or a visible content flash.
Hydration lets you:
- Fetch data once on the server (no extra client round-trip on load).
- Inline the result as JSON in the HTML.
- Have the client’s atom start pre-filled — first render matches server HTML, no flash.
Real-world example — pre-loading the current user (Next.js App Router)
// atoms/user.ts — shared between server and clientimport { atom } from "@mongez/react-atom";
type User = { id: number; name: string; role: "admin" | "viewer" } | null;
export const currentUserAtom = atom<User>({ key: "currentUser", default: null });// app/layout.tsx — SERVER componentimport { createAtomStore } from "@mongez/atom";import { AtomStoreProvider, HydrateAtomsScript } from "@mongez/react-atom";import { getCurrentUser } from "@/lib/auth"; // your server-side auth helperimport { currentUserAtom } from "@/atoms/user";
export default async function RootLayout({ children }: { children: React.ReactNode }) { // Runs on the server — no client round-trip needed const user = await getCurrentUser();
const store = createAtomStore(); store.use(currentUserAtom).update(user); // pre-fill the store
return ( <html lang="en"> <body> <AtomStoreProvider store={store}> {children} {/* Serialises atom state into an inline <script type="application/json"> tag */} <HydrateAtomsScript snapshot={store.snapshot()} /> </AtomStoreProvider> </body> </html> );}// app/providers.tsx — CLIENT boundary"use client";import { AtomStoreProvider, readHydration } from "@mongez/react-atom";
export function ClientProviders({ children }: { children: React.ReactNode }) { // readHydration() parses the JSON script tag injected by the server. // Pass it as initialValues so the client store starts pre-filled. return ( <AtomStoreProvider initialValues={readHydration() ?? undefined}> {children} </AtomStoreProvider> );}// components/UserMenu.tsx — any client component"use client";import { currentUserAtom } from "@/atoms/user";
export function UserMenu() { const user = currentUserAtom.useValue();
// user is never null on first render — the server already fetched it. // No loading spinner, no hydration mismatch. if (!user) return <a href="/login">Sign in</a>; return ( <div> <span>{user.name}</span> {user.role === "admin" && <a href="/admin">Admin</a>} </div> );}Without this pattern currentUserAtom.useValue() would be null on the client’s first render even though the server knew the user — you’d see a brief “Sign in” flicker before the client fetched and re-rendered.
End-to-end minimal example
// server componentimport { createAtomStore } from "@mongez/atom";import { AtomStoreProvider, HydrateAtomsScript } from "@mongez/react-atom";
export default function Layout({ children }) { const store = createAtomStore(); store.use(someAtom).update(serverFetchedValue); return ( <AtomStoreProvider store={store}> {children} <HydrateAtomsScript snapshot={store.snapshot()} /> </AtomStoreProvider> );}// client root"use client";import { AtomStoreProvider, readHydration } from "@mongez/react-atom";
export function ClientRoot({ children }) { return ( <AtomStoreProvider initialValues={readHydration() ?? undefined}> {children} </AtomStoreProvider> );}Concurrent isolation
<AtomStoreProvider> instances are independent. Two providers in different parts of the tree don’t share state — useful for multi-tenant dashboards or A/B split views.
<> <AtomStoreProvider> <TenantA /> </AtomStoreProvider> <AtomStoreProvider> <TenantB /> </AtomStoreProvider></>