| name | mongez-atom-atom-store |
| description | How to use `AtomStore` and `createAtomStore` for per-request SSR isolation — creating scoped atom clones, hydrating snapshots, and tearing down stores after each request.
TRIGGER when: code imports `AtomStore`, `createAtomStore`, or calls `store.use`, `store.get`, `store.has`, `store.list`, `store.hydrate`, `store.snapshot`, `store.destroy` from `@mongez/atom`; user asks "how do I isolate atom state per SSR request", "why are atoms leaking between requests", or "how do I serialize and rehydrate atoms"; `import { createAtomStore, AtomStore } from "@mongez/atom"`.
SKIP: defining the atoms themselves (use `mongez-atom-atoms` or `mongez-atom-defining-atoms`); React-side `AtomStoreProvider` / `useAtomStore` wiring (lives in `@mongez/react-atom`); generic client-only state without SSR (no store needed).
|
AtomStore — SSR Isolation
When to use
Load this skill when the user:
- Runs SSR (Next.js, Remix, Express + React, Fastify) and shares atom state between requests
- Uses
AtomStore or createAtomStore from @mongez/atom
- Asks why two concurrent requests overwrite each other's atoms
- Needs to serialize server-side atom state and send it to the client for hydration
- Is wiring up
<AtomStoreProvider> from @mongez/react-atom
The problem AtomStore solves
Atoms registered with createAtom live in a module-level registry (atoms object in atom.ts). In a Node process serving multiple concurrent requests, every request shares the same atoms. Request A writing userAtom.update({ name: "Alice" }) will corrupt Request B's view of the same atom.
AtomStore gives each request its own isolated clone of every atom template. The template atom itself is never mutated.
Concepts
| Term | Meaning |
|---|
| Template atom | The atom created with createAtom at module level. Holds the default. Never mutated directly in SSR. |
| Scoped clone | A per-store copy of a template atom. Created lazily on first store.use(template). Keyed by the template's original key internally; owns its own state and event subscriptions. |
| Snapshot | A plain Record<string, unknown> — the current values of every scoped atom in the store. Used to pass server state to the client. |
Basic SSR pattern
import { createAtom } from "@mongez/atom";
export const userAtom = createAtom({ key: "user", default: { name: "Anon", loggedIn: false } });
export const cartAtom = createAtom({ key: "cart", default: [] });
export const localeAtom = createAtom({ key: "locale", default: "en" });
import { createAtomStore } from "@mongez/atom";
import { userAtom, cartAtom, localeAtom } from "./state";
async function handleRequest(req, res) {
const store = createAtomStore();
try {
const user = store.use(userAtom);
const locale = store.use(localeAtom);
user.update({ name: req.user.name, loggedIn: true });
locale.update(req.headers["accept-language"]?.slice(0, 2) ?? "en");
const html = renderApp(store);
const snapshot = store.snapshot();
res.send(buildHtml(html, snapshot));
} finally {
store.destroy();
}
}
API reference
createAtomStore / new AtomStore
import { createAtomStore, AtomStore } from "@mongez/atom";
const store = createAtomStore();
store.use(template)
Lazily creates a scoped clone of template. Second call with the same template returns the existing clone.
const scopedUser = store.use(userAtom);
scopedUser.update({ name: "Alice", loggedIn: true });
userAtom.value;
store.get(key)
Look up an already-created scoped atom by its original key. Returns undefined if use() has not been called for that key yet.
store.get("user")?.value;
store.get("never-used");
store.has(key)
store.has("user");
store.list()
All scoped atoms currently in this store (in insertion order).
store.list();
store.hydrate(snapshot)
Apply a snapshot of initial values. Atoms already in the store are updated immediately; atoms not yet registered have their values queued and applied on the next use(template) call.
store.hydrate({
"locale": "fr",
"ui.theme": "dark",
});
const locale = store.use(localeAtom);
locale.value;
store.snapshot()
Serialize current scoped atom values to a plain object. Use for embedding in SSR HTML or passing as initialValues to <AtomStoreProvider>.
const snapshot = store.snapshot();
const html = `<script>window.__ATOM_SNAPSHOT__ = ${JSON.stringify(snapshot)}</script>`;
store.destroy()
Destroys every scoped clone and clears the store. Must be called at the end of each request to prevent event-bus subscription leaks.
store.destroy();
Next.js App Router example
import { createAtomStore } from "@mongez/atom";
import { cache } from "react";
export const getRequestStore = cache(() => createAtomStore());
import { getRequestStore } from "@/lib/atom-store";
import { userAtom } from "@/state";
import { cookies } from "next/headers";
export default async function Page() {
const store = getRequestStore();
const user = store.use(userAtom);
const session = cookies().get("session")?.value;
if (session) {
user.update(JSON.parse(session));
}
const snapshot = store.snapshot();
return <ClientRoot initialValues={snapshot} />;
}
import { AtomStoreProvider } from "@mongez/react-atom";
export function ClientRoot({ initialValues, children }) {
return (
<AtomStoreProvider initialValues={initialValues}>
{children}
</AtomStoreProvider>
);
}
Multiple stores (multi-tenant / parallel rendering)
You can create as many stores as needed. Each is fully independent:
const storeA = createAtomStore();
const storeB = createAtomStore();
storeA.use(userAtom).update({ name: "Alice", loggedIn: true });
storeB.use(userAtom).update({ name: "Bob", loggedIn: true });
storeA.get("user")?.value;
storeB.get("user")?.value;
userAtom.value;
Key pitfalls
- Always call
store.destroy() after each request. Scoped clones subscribe to the @mongez/events bus. Failing to destroy creates an event-listener leak that grows with traffic.
store.use(template) is lazy. If a component reads userAtom.value directly (template, not store.use(userAtom)) on the server, it gets the global default, not the request-scoped value. All server-side reads must go through store.use().
store.hydrate() queues values for atoms not yet used. If you call hydrate before any use(), values are staged. They are applied the first time the corresponding use(template) is called. Order of operations does not matter.
- Clone keys are internal. A scoped clone's key is
${original.key}.clone.N. The store indexes it by the original key (template.key), so store.get("user") works even though the clone's .key property is different.
- Do not use
persist: true with scoped atoms. The built-in localStorage adapter no-ops on the server, but a custom async adapter on a scoped atom would write per-request data to a shared store keyed by the clone's suffixed key. If you need SSR-safe persistence, apply it to the template and use a cookie adapter that reads from req.cookies.