🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mongez/react-atom

Package Overview
Dependencies
Maintainers
1
Versions
66
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mongez/react-atom - npm Package Compare versions

Comparing version
6.0.3
to
6.0.4
+1
-1
esm/store.d.mts

@@ -12,3 +12,3 @@ import React from "react";

*/
declare const AtomStoreContext: React.Context<AtomStore | null>;
declare const AtomStoreContext: React.Context<any>;
type AtomStoreProviderProps = {

@@ -15,0 +15,0 @@ /**

@@ -1,1 +0,1 @@

{"version":3,"file":"store.d.mts","names":[],"sources":["../../../../@mongez/react-atom/src/store.tsx"],"mappings":";;;;;;AAgBA;;;;;cAAa,gBAAA,EAAgB,KAAA,CAAA,OAAA,CAAA,SAAA;AAAA,KAEjB,sBAAA;EAFiB;AAAA;AAE7B;;;EAME,KAAA,GAAQ,SAAA;EAOO;;;;;EAAf,YAAA,GAAe,IAAA;EAPP;;;;;;;;EAiBR,aAAA,GAAgB,MAAA;EAEhB,QAAA,EAAU,KAAA,CAAM,SAAA;AAAA;;;;;;;;;;;;;iBAeF,iBAAA,CAAA;EACd,KAAA;EACA,YAAA;EACA,aAAA;EACA;AAAA,GACC,sBAAA,GAAsB,KAAA,CAAA,GAAA,CAAA,OAAA;;;;;iBAsCT,YAAA,CAAA,GAAgB,SAAS;;;;;;AAtChB;AAsCzB;;;;AAAyC;AAoBzC;;;;;iBAAgB,OAAA,cAAqB,MAAA,mBAAA,CACnC,QAAA,EAAU,IAAA,CAAK,CAAA,EAAG,CAAA,IACjB,IAAA,CAAK,CAAA,EAAG,CAAA;AAAA,iBACK,OAAA,SAAA,CAAiB,GAAA,WAAc,IAAI,CAAC,CAAA"}
{"version":3,"file":"store.d.mts","names":[],"sources":["../../../../@mongez/react-atom/src/store.tsx"],"mappings":";;;;;;AAgBA;;;;AAA6B;cAAhB,gBAAA,EAAgB,KAAA,CAAA,OAAA;AAAA,KAEjB,sBAAA;;;;;;EAMV,KAAA,GAAQ,SAAA;EAmBiB;;;;;EAZzB,YAAA,GAAe,IAAA;EAUC;;;;;AAES;AAe3B;;EAjBE,aAAA,GAAgB,MAAA;EAEhB,QAAA,EAAU,KAAA,CAAM,SAAA;AAAA;;;;;;;;;;;;;iBAeF,iBAAA,CAAA;EACd,KAAA;EACA,YAAA;EACA,aAAA;EACA;AAAA,GACC,sBAAA,GAAsB,KAAA,CAAA,GAAA,CAAA,OAAA;;;;;iBAsCT,YAAA,CAAA,GAAgB,SAAS;;AAtChB;AAsCzB;;;;AAAyC;AAoBzC;;;;;;;;;iBAAgB,OAAA,cAAqB,MAAA,mBAAA,CACnC,QAAA,EAAU,IAAA,CAAK,CAAA,EAAG,CAAA,IACjB,IAAA,CAAK,CAAA,EAAG,CAAA;AAAA,iBACK,OAAA,SAAA,CAAiB,GAAA,WAAc,IAAI,CAAC,CAAA"}

@@ -27,3 +27,3 @@ {

},
"version": "6.0.3",
"version": "6.0.4",
"main": "./cjs/index.cjs",

@@ -30,0 +30,0 @@ "module": "./esm/index.mjs",

+557
-150

@@ -0,153 +1,464 @@

<div align="center">
# @mongez/react-atom
> React adapter for [`@mongez/atom`](https://github.com/hassanzohdy/atom). Hooks on every atom, `<AtomStoreProvider>` for SSR isolation, preset atoms for the common 80% of state work, hydration helpers, and `useSyncExternalStore` under the hood so reads are tear-free under React 18 concurrent rendering.
**React hooks layer on top of [`@mongez/atom`](https://github.com/hassanzohdy/atom) — every atom carries its own `useValue` / `useState` / `use(key)` / `useWatch` / `<Provider>`, plus preset atoms for toggles, loading flags, fetch lifecycles, and modal portals, all SSR-safe.**
## Install
[![npm](https://img.shields.io/npm/v/@mongez/react-atom.svg)](https://www.npmjs.com/package/@mongez/react-atom)
[![license](https://img.shields.io/npm/l/@mongez/react-atom.svg)](LICENSE)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@mongez/react-atom.svg)](https://bundlephobia.com/package/@mongez/react-atom)
[![downloads](https://img.shields.io/npm/dw/@mongez/react-atom.svg)](https://www.npmjs.com/package/@mongez/react-atom)
</div>
---
## Why @mongez/react-atom?
`useState` is component-local — sharing a value across siblings means lifting it or wiring a context every time. `Zustand` and `Jotai` solve sharing but each invent their own `create` / `atom()` shape, their own hook conventions (`useStore`, `useAtom`/`useAtomValue`/`useSetAtom`), and their own escape hatches for SSR. `Recoil` is heavier, less actively maintained, and still requires you to write atoms and selectors with selector wrappers.
`@mongez/react-atom` is the React hooks layer on top of [`@mongez/atom`](https://github.com/hassanzohdy/atom) — the same framework-agnostic atom you already know, just with hooks attached as instance methods. **One atom, multiple hooks on it.** No `useAtomValue` / `useSetAtom` distinction, no selector wrappers, no second mental model. All built on `useSyncExternalStore` so reads are tear-free under React 18 concurrent rendering, and an `<AtomStoreProvider>` ships in the box for per-request SSR isolation.
```tsx
import { atom } from "@mongez/react-atom";
const counterAtom = atom({ key: "counter", default: 0 });
function Counter() {
const count = counterAtom.useValue();
return (
<button onClick={() => counterAtom.update(count + 1)}>
Count: {count}
</button>
);
}
```
---
## Features
| Feature | Description |
|---|---|
| **Hooks on every atom** | `useValue`, `useState`, `use(key)`, `useWatch`, and `<Provider>` are instance methods on the atom — no `useAtomValue` / `useSetAtom` split. |
| **Granular subscriptions** | `atom.use("name")` re-renders only when `name` changes; sibling keys in the same atom don't wake the component. |
| **Tear-free under React 18** | Every hook is built on `useSyncExternalStore` — concurrent rendering can't show two components disagreeing on the same atom. |
| **Stable setter identity** | `useState()` returns a setter with the same identity across renders — safe in `useEffect` / `useCallback` deps without looping. |
| **Preset atoms** | `openAtom`, `loadingAtom`, `fetchingAtom`, and `portalAtom` collapse 80% of state work to one line. |
| **`<AtomStoreProvider>`** | Per-subtree atom isolation — each SSR request gets its own scoped clones. Concurrent requests can't see each other's state. |
| **Hydration helpers** | `<HydrateAtomsScript>`, `readHydration()`, and `serializeStore` ship a server-to-client transport with `</script>` and U+2028/U+2029 escaped. |
| **TypeScript-first** | Atom value and action types flow into every hook; `use("name")` is typed to the keys of the atom's value. |
| **Backward-compatible** | Legacy `<AtomProvider>` and `AtomContext` survive as deprecated shims while you migrate. |
| **Zero registry coupling** | Module-level singleton mode is the default — providers are opt-in for SSR and isolation. |
---
## Installation
```sh
npm install @mongez/react-atom
```
```sh
yarn add @mongez/react-atom
# peer: react >= 18, @mongez/atom
```
## A 30-second tour
```sh
pnpm add @mongez/react-atom
```
Peer dependencies: `react >= 18`. The package depends on [`@mongez/atom`](https://github.com/hassanzohdy/atom) directly — installing this package also installs that core layer.
> **React 18 or newer is mandatory.** `useSyncExternalStore` is required, and there is no plan to backport. If you're on React 16/17, stay on `@mongez/react-atom@5`.
---
## Quick start
Create an atom, mount its hooks anywhere — no provider, no context wiring.
```tsx
import {
atom,
openAtom,
loadingAtom,
fetchingAtom,
portalAtom,
} from "@mongez/react-atom";
// atoms/counter.ts
import { atom } from "@mongez/react-atom";
// 1. Any atom gets per-atom hooks
const counter = atom({ key: "counter", default: 0 });
export const counterAtom = atom({
key: "counter",
default: 0,
actions: {
increment() { this.update(this.value + 1); },
decrement() { this.update(this.value - 1); },
reset() { this.update(0); },
},
});
```
```tsx
// components/Counter.tsx
import { counterAtom } from "../atoms/counter";
export function Counter() {
const count = counterAtom.useValue();
return (
<div>
<button onClick={() => counterAtom.decrement()}>−</button>
<span>{count}</span>
<button onClick={() => counterAtom.increment()}>+</button>
<button onClick={() => counterAtom.reset()}>reset</button>
</div>
);
}
```
Any other component that calls `counterAtom.useValue()` subscribes to the same value — no lifting, no context. That's the entire surface for client-only apps. Everything below is depth on the same handful of exports.
---
## Atoms and their hooks
`atom(options)` is the React-aware version of `createAtom` from `@mongez/atom`. The returned atom carries every base method (`update`, `merge`, `change`, `onChange`, ...) plus a small set of hooks bound to the instance.
### `atom.useValue()`
Subscribes to the atom; returns the current value; re-renders the component whenever the value changes.
```tsx
const user = userAtom.useValue();
return <h1>Hello {user.name}</h1>;
```
### `atom.useState()`
Returns `[value, setValue]` — the same shape as React's `useState`, but the value lives in the atom and is visible to every other subscriber.
```tsx
const [count, setCount] = counterAtom.useState();
return <button onClick={() => setCount(count + 1)}>{count}</button>;
```
The setter accepts an updater function: `setCount(prev => prev + 1)`.
> **The setter's identity is stable across renders.** Safe to list in `useEffect` / `useCallback` dependency arrays without re-triggering or looping. This is the React-18 rebuild paying off — pre-v6 code that put the setter in deps used to misbehave.
**Prefer `useValue()` + calling the atom's methods directly.** It keeps updates explicit and traceable, especially when paired with custom actions:
```tsx
const counterAtom = atom({
key: "counter",
default: 0,
actions: { increment() { this.update(this.value + 1); } },
});
function Counter() {
const [count, setCount] = counter.useState();
return <button onClick={() => setCount(count + 1)}>{count}</button>;
const count = counterAtom.useValue();
return <button onClick={() => counterAtom.increment()}>{count}</button>;
}
```
// 2. Preset atoms collapse common patterns
const sidebar = openAtom("sidebar");
sidebar.toggle();
const opened = sidebar.useOpened(); // → boolean
### `atom.use(key)`
const loginPending = loadingAtom("loginPending");
loginPending.startLoading();
For object-valued atoms: subscribes to **one key only**. Re-renders only when that key changes — sibling keys in the same atom don't wake the component.
const usersFetch = fetchingAtom<User[]>("users");
usersFetch.success(data); // sets isLoading=false, data
const isLoading = usersFetch.useLoading();
const users = usersFetch.useData();
```tsx
const profileAtom = atom({
key: "profile",
default: { name: "Anon", email: "", lastSeen: 0 },
});
// 3. Modals with payload
const editUserModal = portalAtom<{ id: number; name: string }>("editUser");
editUserModal.open({ id: 7, name: "Alice" });
const opened = editUserModal.useOpened();
const data = editUserModal.useData();
function NameDisplay() {
const name = profileAtom.use("name");
return <span>{name}</span>;
}
// Updating lastSeen does NOT re-render NameDisplay.
setInterval(() => profileAtom.change("lastSeen", Date.now()), 1000);
```
## What's in the box
`use(key)` is typed against the atom's value shape — `profileAtom.use("nope")` is a compile error.
| Export | Purpose |
|---|---|
| `atom`, `atomCollection` | React-aware atom factories (wrap `@mongez/atom`). |
| `openAtom`, `loadingAtom`, `fetchingAtom`, `portalAtom` | Preset atoms for common state shapes. |
| `<AtomStoreProvider>` | Scopes atoms per subtree (per SSR request, per test, etc.). |
| `useAtom(template \| key)` | Resolve to store-scoped clone (or template). |
| `useAtomStore()` | Returns the active `AtomStore \| null`. |
| `<HydrateAtomsScript>` | Emits a typed `<script>` tag with the snapshot. |
| `readHydration()` | Client-side picker for the embedded snapshot. |
| `serializeSnapshot`, `serializeStore` | XSS-safe JSON serializers. |
### `atom.useWatch(key, callback)`
## Hooks every atom carries
Effect-style watcher. Fires the callback when the key changes, but **doesn't re-render the host component**. Use for analytics, logging, persistence side-effects.
```ts
const userAtom = atom({ key: "user", default: { name: "Anon", age: 0 } });
```tsx
function AnalyticsTracker() {
userAtom.useWatch("name", (next, prev) => {
analytics.track("name_changed", { from: prev, to: next });
});
return null;
}
```
userAtom.useValue(); // → V, re-renders on any change
userAtom.useState(); // → [V, (next) => void], stable setter
userAtom.use("name"); // → V[K], subscribes to ONE key only
userAtom.useWatch("name", cb); // effect-style; doesn't re-render this component
<userAtom.Provider value={initial}>...</userAtom.Provider>
### `<atom.Provider value={…}>`
A React component that pushes a value into the atom on mount and on `value` prop change. Useful for testing (inject a known value into the subtree) and for "set on mount" patterns where state comes from props.
```tsx
function App({ initialTheme }) {
return (
<themeAtom.Provider value={initialTheme}>
<RestOfApp />
</themeAtom.Provider>
);
}
```
All built on `useSyncExternalStore`. Reads are tear-free under React 18+ concurrent rendering. The setter returned by `useState()` is a **stable function** across renders — safe to put in `useEffect` / `useCallback` deps without looping.
> **`<atom.Provider>` updates via `useEffect`** — the value is applied after first paint, not synchronously during render. The first frame still reads the atom's pre-existing value. For SSR-correct pre-fill, use `<AtomStoreProvider initialValues={...}>` instead.
When an `<AtomStoreProvider>` is mounted above, these hooks operate on the store-scoped clone. Without a provider, they operate on the module-level singleton — the correct default for client-only SPAs.
### `atomCollection(options)`
The React-aware version of `atomCollection` from `@mongez/atom`. Inherits the array helpers (`push`, `unshift`, `pop`, `shift`, `remove`, `replace`, ...) AND every hook above.
```tsx
import { atomCollection } from "@mongez/react-atom";
type Todo = { id: number; text: string; done: boolean };
const todosAtom = atomCollection<Todo>({ key: "todos", default: [] });
todosAtom.push({ id: 1, text: "Buy bread", done: false });
function TodoList() {
const todos = todosAtom.useValue();
return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}
```
> **Naming convention:** suffix atom variables with `Atom` (e.g. `counterAtom`, `userAtom`) — prevents collisions with locals and makes it obvious at call sites that you're working with a reactive value.
---
## Preset atoms
### `openAtom`
Four pre-built atom shapes that collapse the most common state patterns to one line. They're regular atoms underneath — every base method and every React hook still works.
### `openAtom(key, defaultOpened?)`
A boolean-shaped toggle with `open`, `close`, `toggle`, and a `useOpened()` hook.
```tsx
import { openAtom } from "@mongez/react-atom";
const sidebarAtom = openAtom("sidebar", /* defaultOpened */ false);
function ToggleButton() {
return <button onClick={() => sidebarAtom.toggle()}>≡</button>;
}
function Sidebar() {
const opened = sidebarAtom.useOpened();
return opened ? <Drawer /> : null;
}
```
### `loadingAtom(key, defaultLoading?)`
A boolean with `startLoading` / `stopLoading` / `toggleLoading` verbs. Use it when you just need a flag — for richer fetch state, reach for `fetchingAtom`.
```tsx
import { loadingAtom } from "@mongez/react-atom";
const usersLoadingAtom = loadingAtom("usersLoading");
async function fetchUsers() {
usersLoadingAtom.startLoading();
try {
const users = await api.users.list();
setUsers(users);
} finally {
usersLoadingAtom.stopLoading();
}
}
```
### `fetchingAtom<DataType, PaginationType>(key, defaultValue?, defaultFetching?)`
A full fetch lifecycle in one atom. Value shape: `{ isLoading, data, error, pagination? }`. Actions: `startLoading`, `stopLoading`, `success(data, pagination?)`, `failed(error)`, `append(data)`, `prepend(data)`. Hooks: `useLoading()`, `useData()`, `useError()`, `usePagination()`. See the [paginated fetch recipe](#wire-a-paginated-fetch-with-fetchingatom) below for a full flow.
`defaultFetching` is `true` by default — the atom starts in a loading state so the first render shows a spinner even before `startLoading()` is called.
> **`fetchingAtom` is for one-off fetch flows, not a query cache.** It has no key-based deduping, no automatic refetch, no stale-while-revalidate. For server-state caching with invalidation, refetch-on-focus, and optimistic updates, use [`@mongez/atomic-query`](https://github.com/hassanzohdy/mongez-atomic-query).
### `portalAtom<T>(name, opened?)`
A coordinator atom for modals, drawers, tooltips, dropdowns. Holds `{ opened: boolean, data: T }` and exposes `open(data?)`, `close()`, `toggle(data?)`, `useOpened()`, `useData()`. See the [modal recipe](#open-a-modal-from-anywhere-with-portalatom) below for a full trigger/consumer pair.
> **The portal atom's internal key is suffixed with `-portal`.** `portalAtom("deleteUser")` registers an atom whose key is `deleteUser-portal`. This is only visible when hydrating by key — the variable you exported still works as expected.
---
## SSR — `<AtomStoreProvider>`
The module-level `atoms` registry is shared per Node process. Two concurrent SSR requests on the same server would see each other's mutations — a classic SSR state-leakage bug. `<AtomStoreProvider>` solves this by giving each request its own scoped clone of every atom touched inside it.
```tsx
import { AtomStoreProvider } from "@mongez/react-atom";
<AtomStoreProvider>
<App />
</AtomStoreProvider>
```
Without a provider, atoms fall back to the module-level singleton — the correct default for client-only SPAs.
### Props
```ts
const m = openAtom("sidebar", /* defaultOpened */ false);
m.open(); m.close(); m.toggle();
const opened = m.useOpened();
type AtomStoreProviderProps = {
store?: AtomStore; // bring your own; otherwise auto-created
initialAtoms?: Atom<any>[]; // pre-register these in the store
initialValues?: Record<string, unknown>; // hydration values keyed by atom key
children: React.ReactNode;
};
```
### `loadingAtom`
| Prop | Behaviour | Note |
|---|---|---|
| `store` | An existing `AtomStore` to use. | Caller owns the lifecycle — the provider does NOT destroy it on unmount. |
| `initialAtoms` | Atom templates to pre-register on first render. | Required if you want `initialValues` to apply to atoms that no component has touched yet. |
| `initialValues` | Initial values keyed by atom key. | Applied via silent update so the first render of subscribers sees the hydrated values — no flash. |
When `store` is omitted, the provider auto-creates one and **destroys it on unmount.**
### Per-request isolation
Two providers in the same tree are fully independent — useful for multi-tenant dashboards, A/B splits, or per-route boundaries.
```tsx
<>
<AtomStoreProvider>
<TenantA />
</AtomStoreProvider>
<AtomStoreProvider>
<TenantB />
</AtomStoreProvider>
</>
```
### `useAtom(template | key)` and `useAtomStore()`
```ts
const l = loadingAtom("fetching.users");
l.startLoading(); l.stopLoading(); l.toggleLoading();
useAtom<V, A>(template: Atom<V, A>): Atom<V, A>; // → store-scoped clone (or template if no provider)
useAtom<V>(key: string): Atom<V> | undefined; // → scoped atom by key, or undefined
useAtomStore(): AtomStore | null; // → the active store
```
### `fetchingAtom`
The per-atom hooks (`useValue`, `useState`, `use`, `useWatch`) already do this resolution internally — you only need `useAtom(template)` explicitly when calling an action method from an event handler under SSR. See the [SSR-safe action recipe](#call-an-action-method-safely-under-ssr) below. The string overload is a legacy escape hatch left over from v5; prefer the template form.
Full fetch lifecycle: `isLoading`, `data`, `error`, optional `pagination`.
`useAtomStore()` returns the active `AtomStore` or `null` — reach for it when you need to snapshot on demand, manually hydrate from a custom transport, or integrate with a non-React layer.
### Hydration helpers
```ts
const usersAtom = fetchingAtom<User[]>("users");
serializeSnapshot(snapshot, options?): string
serializeStore(store, options?): string
readHydration(id?: string): Record<string, unknown> | null
async function loadUsers() {
usersAtom.startLoading();
try {
const data = await api.users.list();
usersAtom.success(data);
} catch (err) {
usersAtom.failed(err);
}
<HydrateAtomsScript snapshot={…} id?={…} nonce?={…} />
```
| Helper | Purpose |
|---|---|
| `serializeSnapshot` | XSS-safe JSON for a `<script>` tag — escapes `</script>` and U+2028/U+2029. |
| `serializeStore` | Convenience over `serializeSnapshot(store.snapshot())`. |
| `<HydrateAtomsScript>` | Renders an inline `<script type="application/json">` carrying the snapshot. Place it once per provider. |
| `readHydration` | Client-side picker that parses the script tag. Returns `null` if missing or malformed. |
| `DEFAULT_HYDRATION_SCRIPT_ID` | The DOM id `<HydrateAtomsScript>` and `readHydration` agree on: `"__mongez_atom_state"`. |
> **Skip these helpers if your framework already has a typed server-to-client transport.** Next.js (`__NEXT_DATA__`), Remix (`useLoaderData`), and TanStack Start (route `loader`) all have their own. Feed the snapshot straight into `<AtomStoreProvider initialValues={…}>` instead of round-tripping through an inline script.
---
## Recipes
### Share a user atom across components with granular subscriptions
Reach for this when multiple components consume slices of the same value. `useValue()` re-renders on every change; `use("name")` re-renders only when that single key changes — important for fields that update frequently (lastSeen, presence, cursor position).
```tsx
// atoms/user.ts
import { atom } from "@mongez/react-atom";
type User = { id: number; name: string; email: string; lastSeen: number };
export const currentUserAtom = atom<User | null>({
key: "currentUser",
default: null,
actions: {
async signIn(email: string, password: string) {
this.update(await api.auth.signIn({ email, password }));
},
signOut() { this.update(null); },
},
});
```
```tsx
// components/Header.tsx — re-renders on ANY field change
import { currentUserAtom } from "../atoms/user";
export function Header() {
const user = currentUserAtom.useValue();
if (!user) return <a href="/login">Sign in</a>;
return <span>Hello, {user.name}</span>;
}
function UsersList() {
const isLoading = usersAtom.useLoading();
const data = usersAtom.useData();
const error = usersAtom.useError();
if (isLoading) return <Spinner />;
if (error) return <ErrorBox e={error} />;
return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
// components/AccountEmail.tsx — re-renders ONLY when `email` changes.
// Frequent `lastSeen` updates don't wake this component.
export function AccountEmail() {
const email = currentUserAtom.use("email");
return <small>{email}</small>;
}
export function SignOutButton() {
return <button onClick={() => currentUserAtom.signOut()}>Sign out</button>;
}
```
For server-data caching with invalidation/refetch/optimistic updates, use [`@mongez/atomic-query`](https://github.com/hassanzohdy/mongez-atomic-query) instead.
### Open a modal from anywhere with `portalAtom`
### `portalAtom`
Reach for this when a modal, drawer, or dialog opens from list rows, header buttons, or keyboard shortcuts and you don't want to lift its open/close state to a common ancestor. The trigger and the modal don't need to share a parent — the atom is the bridge.
A coordinator atom for modals, drawers, tooltips. Holds `{ opened: boolean, data: T }`.
```tsx
// atoms/modals.ts
import { portalAtom } from "@mongez/react-atom";
export const editUserModal = portalAtom<{ id: number; initialName: string }>("editUser");
```
```tsx
const deleteUserModal = portalAtom<{ userId: number; userName: string }>("deleteUser");
// components/UserRow.tsx — trigger
import { editUserModal } from "../atoms/modals";
// Anywhere:
<button onClick={() => deleteUserModal.open({ userId: 7, userName: "Alice" })}>
Delete
</button>
function UserRow({ user }) {
return (
<tr>
<td>{user.name}</td>
<td>
<button onClick={() => editUserModal.open({ id: user.id, initialName: user.name })}>
Edit
</button>
</td>
</tr>
);
}
```
// In the modal:
function ConfirmDelete() {
const opened = deleteUserModal.useOpened();
const { userId, userName } = deleteUserModal.useData();
```tsx
// components/EditUserModal.tsx — render once at the app root
import { editUserModal } from "../atoms/modals";
export function EditUserModal() {
const opened = editUserModal.useOpened();
const data = editUserModal.useData();
if (!opened) return null;
return (
<Dialog>
<p>Delete {userName}?</p>
<button onClick={() => deleteUserModal.close()}>Cancel</button>
<button onClick={async () => {
await api.users.remove(userId);
deleteUserModal.close();
<Dialog onClose={() => editUserModal.close()}>
<h2>Edit user</h2>
<form onSubmit={async (e) => {
e.preventDefault();
await api.users.update(data.id, /* ... */);
editUserModal.close();
}}>
Delete
</button>
<input defaultValue={data.initialName} />
<button>Save</button>
</form>
</Dialog>

@@ -158,44 +469,76 @@ );

## SSR — `<AtomStoreProvider>`
### Wire a paginated fetch with `fetchingAtom`
The module-level `atoms` registry is shared per Node process. Two concurrent SSR requests would see each other's state. `<AtomStoreProvider>` gives each request its own scoped clones.
Reach for this when a single endpoint feeds a single piece of UI and you don't need cache invalidation or refetch — settings forms, dashboard panels, detail pages. `success`, `failed`, `append`, and `prepend` each set `isLoading: false` in one merge — no `finally` wrapper needed.
```tsx
import { AtomStoreProvider } from "@mongez/react-atom";
// atoms/products.ts
import { fetchingAtom } from "@mongez/react-atom";
<AtomStoreProvider>
<App />
</AtomStoreProvider>
type Product = { id: number; name: string; price: number };
type Pagination = { page: number; pages: number };
export const productsAtom = fetchingAtom<Product[], Pagination>("products");
export async function loadProducts(page = 1) {
productsAtom.startLoading();
try {
const { data, pagination } = await api.products.list({ page });
productsAtom.success(data, pagination);
} catch (err) {
productsAtom.failed(err);
}
}
```
Props:
```tsx
// components/ProductsList.tsx
import { productsAtom, loadProducts } from "../atoms/products";
```ts
type AtomStoreProviderProps = {
store?: AtomStore; // bring your own; caller-owned lifecycle
initialAtoms?: Atom<any>[]; // pre-register
initialValues?: Record<string, unknown>; // silent-update on entry
children: React.ReactNode;
};
export function ProductsList() {
const isLoading = productsAtom.useLoading();
const products = productsAtom.useData();
const error = productsAtom.useError();
React.useEffect(() => { loadProducts(); }, []);
if (error) return <p className="text-red-500">{String(error)}</p>;
if (!products && isLoading) return <Spinner />;
return <ul>{products?.map(p => <li key={p.id}>{p.name} — ${p.price}</li>)}</ul>;
}
```
Without a `store` prop, the provider auto-creates one and destroys it on unmount.
### Hydrate atoms in SSR (Next.js App Router)
### End-to-end pattern (Next.js App Router)
Reach for this when you need the first client render to match the server HTML — current user, feature flags, initial page data. Without hydration the client starts from `default` and renders a loading state (or a different UI than the server), causing a hydration mismatch or visible flash.
```tsx
// app/layout.tsx (server component)
// 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 { userAtom } from "@/state/user";
import { getCurrentUser } from "@/lib/auth";
import { currentUserAtom } from "@/atoms/user";
export default async function Layout({ children }) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser(); // server-only round-trip
const store = createAtomStore();
store.use(userAtom).update(await getCurrentUser());
store.use(currentUserAtom).update(user); // pre-fill the store
return (
<AtomStoreProvider store={store}>
{children}
<HydrateAtomsScript snapshot={store.snapshot()} />
</AtomStoreProvider>
<html lang="en">
<body>
<AtomStoreProvider store={store}>
{children}
<HydrateAtomsScript snapshot={store.snapshot()} />
</AtomStoreProvider>
</body>
</html>
);

@@ -206,7 +549,7 @@ }

```tsx
// app/client-root.tsx
// app/providers.tsx — CLIENT boundary
"use client";
import { AtomStoreProvider, readHydration } from "@mongez/react-atom";
export function ClientRoot({ children }) {
export function ClientProviders({ children }: { children: React.ReactNode }) {
return (

@@ -220,16 +563,19 @@ <AtomStoreProvider initialValues={readHydration() ?? undefined}>

Same pattern works for Remix (`loader` + `useLoaderData`) and TanStack Start (route `loader`).
`currentUserAtom.useValue()` is never `null` on first render now — the server already fetched it and the client store starts pre-filled. Same pattern works for Remix (load in a `loader`, pass via `useLoaderData`) and TanStack Start (route `loader` plus a client root); skip `<HydrateAtomsScript>` / `readHydration()` in those and feed the snapshot straight into `initialValues`.
## `useAtom(template | key)`
### Call an action method safely under SSR
```ts
useAtom<V, A>(template: Atom<V, A>): Atom<V, A>;
useAtom<V>(key: string): Atom<V> | undefined;
```
Reach for this when an event handler calls an action method (`auth.logout()`, `cart.checkout()`) inside a component rendered under `<AtomStoreProvider>`. Calling the module-level atom directly mutates the template, which is shared across concurrent SSR requests — a leak between users. Use `useAtom(template)` to grab the per-request scoped clone instead.
**Template overload** → store-scoped clone (or the template itself when no provider is mounted). Use it when calling action methods from an event handler so they hit the scoped clone in SSR-safe code:
```tsx
import { useAtom } from "@mongez/react-atom";
```tsx
// Wrong under SSR — mutates the shared module-level template.
function BadLogoutButton() {
return <button onClick={() => authAtom.logout()}>Log out</button>;
}
// Right — resolves to the per-request scoped clone under a provider.
function LogoutButton() {
const auth = useAtom(authAtom); // resolves to the active store's clone
const auth = useAtom(authAtom);
return <button onClick={() => auth.logout()}>Log out</button>;

@@ -239,46 +585,107 @@ }

The per-atom hooks (`useValue`, `useState`, `use`, `useWatch`) already do this resolution internally — you only need `useAtom(template)` explicitly when calling action methods.
On the client (no provider), `useAtom(template)` falls back to the template itself, so the safe form works everywhere. The per-atom hooks (`useValue`, `useState`, `use`, `useWatch`) already do this resolution internally — you only need `useAtom(template)` when calling action methods outside a hook.
**String overload** → look up a scoped atom by string key. Legacy escape hatch; prefer the template form.
### Isolate atom state per test
## Hydration helpers
Reach for this in unit tests when you want every test to start from a clean slate. Wrapping each render in `<AtomStoreProvider>` gives every test its own scoped store — the module-level template stays untouched between runs.
```ts
serializeSnapshot(snapshot, options?): string
serializeStore(store, options?): string
readHydration(id?: string): Record<string, unknown> | null
```tsx
// __tests__/UserBadge.test.tsx
import { render, screen } from "@testing-library/react";
import { AtomStoreProvider } from "@mongez/react-atom";
import { currentUserAtom } from "../atoms/user";
import { UserBadge } from "../components/UserBadge";
<HydrateAtomsScript snapshot={...} id? nonce? />
test("renders user name when atom is pre-filled", () => {
render(
<AtomStoreProvider
initialValues={{ currentUser: { id: 1, name: "Alice", email: "a@x.com" } }}
>
<UserBadge />
</AtomStoreProvider>,
);
expect(screen.getByText("Alice")).toBeInTheDocument();
});
test("renders 'Guest' when atom is at default", () => {
render(
<AtomStoreProvider>
<UserBadge />
</AtomStoreProvider>,
);
expect(screen.getByText("Guest")).toBeInTheDocument();
});
```
`serializeSnapshot` escapes `</script>` and U+2028/U+2029 so the payload is safe inside an inline `<script type="application/json">`. `<HydrateAtomsScript>` renders the script tag for you; `readHydration()` is the client-side picker.
For a quick subtree pre-fill without an entire `AtomStoreProvider`, `<currentUserAtom.Provider value={…}>` also works — but it pushes the value via `useEffect`, so the first frame still reads the previous value. `<AtomStoreProvider initialValues={…}>` applies synchronously before first render.
## Devtools
### Enable Redux DevTools
Reach for this in development builds when you want to see every atom update in the Redux DevTools extension — preset atoms, fetch atoms, and portal atoms all show up.
```ts
// app entry — runs once at boot
import { enableAtomDevtools } from "@mongez/atom";
if (process.env.NODE_ENV !== "production") {
enableAtomDevtools({ name: "MyApp" });
enableAtomDevtools({ name: "MyReactApp" });
}
```
Pipes every atom (including preset atoms, fetch atoms, portal atoms) into the Redux DevTools extension. Time-travel works.
Time-travel works: rewinding a `fetchingAtom` from `success` back to `startLoading` rolls the connected `UsersList` back to its spinner state.
## What this package does NOT do
> **Strip the import in production.** The bridge subscribes to every atom and serialises every update — fine in dev, wasteful in a shipped bundle. The `process.env.NODE_ENV` check above is the standard guard.
- The atom primitive itself → [`@mongez/atom`](https://github.com/hassanzohdy/atom)
- Server-state caching with query keys / invalidation / refetch → [`@mongez/atomic-query`](https://github.com/hassanzohdy/mongez-atomic-query)
- The event bus → [`@mongez/events`](https://github.com/hassanzohdy/events)
---
## React version
## TypeScript
React **18 or newer**. `useSyncExternalStore` is required. There are no plans to support 16/17; those users should stay on `@mongez/react-atom@5`.
Atom value and action types flow into every hook automatically. `ReactAtom<V, A>` extends `Atom<V, A>` from `@mongez/atom` with the React-aware actions; type generic helpers that read from an atom via a hook as `ReactAtom<V, A>` rather than the base `Atom<V, A>`.
```ts
import type {
ReactAtom,
ReactActions,
OpenAtomActions, OpenAtomType,
LoadingAtom, LoadingAtomActions,
FetchingAtomType, FetchingAtomActions,
AtomPortal, PortalActions,
AtomStoreProviderProps, HydrateAtomsScriptProps,
} from "@mongez/react-atom";
function useFirstChar<V extends { name: string }>(atom: ReactAtom<V>) {
return atom.use("name")[0]; // `use("nope")` would be a compile error
}
```
---
## Migration
See [`MIGRATION.md`](./MIGRATION.md) for the v5 → v6 transition.
The v5 → v6 jump moves the package to React 18+ only and rebuilds every hook on `useSyncExternalStore` for tear-free concurrent reads. The `<AtomStoreProvider>` surface is new; legacy `<AtomProvider>` / `AtomContext` survive as deprecated shims that translate to the new types. Full diff and step-by-step migration in [`MIGRATION.md`](./MIGRATION.md).
---
## Related packages
| Package | Use when you need |
|---|---|
| [`@mongez/atom`](https://github.com/hassanzohdy/atom) | The framework-agnostic atom primitive this package wraps. Use it directly in vanilla / Vue / non-React code, or alongside this package for stores, `derive`, and persistence. |
| [`@mongez/atomic-query`](https://github.com/hassanzohdy/mongez-atomic-query) | Server-state cache for React with query keys, invalidation, refetch-on-focus, and optimistic updates. Built on top of this package — use it instead of hand-rolled `fetchingAtom` flows once you outgrow them. |
| [`@mongez/events`](https://github.com/hassanzohdy/events) | Cross-feature pub/sub. The atom's `onChange` covers most local subscription needs; reach for events when multiple unrelated features need to coordinate. |
| [`@mongez/cache`](https://github.com/hassanzohdy/mongez-cache) | Pluggable cache facade — drop into `@mongez/atom`'s `persist` slot for free localStorage / encrypted / runtime persistence on atoms shared with this package. |
---
## Further reading
- [`MIGRATION.md`](./MIGRATION.md) — v5 → v6 transition guide.
- [`CHANGELOG.md`](./CHANGELOG.md) — release notes.
- [`llms-full.txt`](./llms-full.txt) — exhaustive single-file API surface for tool-assisted development.
- [`skills/`](./skills) — per-topic deep-dives (atoms, presets, SSR, recipes).
---
## License
MIT