How to use `AtomStoreProvider`, `useAtom`, `useAtomStore`, and the hydration utilities (`HydrateAtomsScript`, `readHydration`, `serializeStore`, `serializeSnapshot`) for SSR-safe, per-request atom isolation and client hydration.
9 stars
0 votes
0 copies
7 views
Added May 28, 2026
developmentgoreactnextjsnode
Works with
cli
Security analysis
A100/100
Scanned September 30, 2026
$npx -y skills add hassanzohdy/mongez-react-atom --skill ssr --agent claude-code
Installs into .claude/skills of the current project.
Are you the author of Ssr?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/hassanzohdy-ssr)
---
name: mongez-react-atom-ssr
description: |
How to use `AtomStoreProvider`, `useAtom`, `useAtomStore`, and the hydration utilities (`HydrateAtomsScript`, `readHydration`, `serializeStore`, `serializeSnapshot`) for SSR-safe, per-request atom isolation and client hydration.
---
# 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>`
```tsx
import { AtomStoreProvider } from "@mongez/react-atom";
<AtomStoreProvider>
<App />
</AtomStoreProvider>
```
Props:
```ts
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` (or `initialValues`), never from the template's current value.
- A `store` passed via props is caller-owned (NOT destroyed by the provider).
- `initialValues` are applied via `silentUpdate` so the first render of subscribers sees the hydrated values; no flash.
## `useAtom(template | key)`
Two overloads.
```ts
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 undefined
```
The 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:
```tsx
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
- `getServerSnapshot` returns 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: true` on `atom()` is read after mount (`persistHydration: "deferred"`), never at creation and never on the server. Scoped clones are not persisted.
- `use("key")` and `watch` fire for atoms declared with `default: null` once 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
```ts
serializeSnapshot(snapshot: Record<string, unknown>, options?: { replacer, space }): string
serializeStore(store: AtomStore, options?): string
readHydration(id?: string): Record<string, unknown> | null
<HydrateAtomsScript
snapshot={snapshotOrString}
id="__mongez_atom_state" // optional; default constant
nonce={cspNonce} // optional
/>
```
- `serializeSnapshot` / `serializeStore` produce 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. Returns `null` if 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:
1. Fetch data once on the server (no extra client round-trip on load).
2. Inline the result as JSON in the HTML.
3. 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)
```ts
// atoms/user.ts — shared between server and client
import { atom } from "@mongez/react-atom";
type User = { id: number; name: string; role: "admin" | "viewer" } | null;
export const currentUserAtom = atom<User>({ key: "currentUser", default: null });
```
```tsx
// app/layout.tsx — SERVER component
import { createAtomStore } from "@mongez/atom";
import { AtomStoreProvider, HydrateAtomsScript } from "@mongez/react-atom";
import { getCurrentUser } from "@/lib/auth"; // your server-side auth helper
import { 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>
);
}
```
```tsx
// 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>
);
}
```
```tsx
// 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
```tsx
// server component
import { 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>
);
}
```
```tsx
// 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.
```tsx
<>
<AtomStoreProvider>
<TenantA />
</AtomStoreProvider>
<AtomStoreProvider>
<TenantB />
</AtomStoreProvider>
</>
```