@mongez/react-atom
Version:
A simple state management tool for React Js.
688 lines (507 loc) • 27.6 kB
Markdown
<div align="center">
# @mongez/react-atom
**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.**
[](https://www.npmjs.com/package/@mongez/react-atom)
[](LICENSE)
[](https://bundlephobia.com/package/@mongez/react-atom)
[](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
```
```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
// atoms/counter.ts
import { atom } from "@mongez/react-atom";
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 = counterAtom.useValue();
return <button onClick={() => counterAtom.increment()}>{count}</button>;
}
```
### `atom.use(key)`
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.
```tsx
const profileAtom = atom({
key: "profile",
default: { name: "Anon", email: "", lastSeen: 0 },
});
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);
```
`use(key)` is typed against the atom's value shape — `profileAtom.use("nope")` is a compile error.
### `atom.useWatch(key, callback)`
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.
```tsx
function AnalyticsTracker() {
userAtom.useWatch("name", (next, prev) => {
analytics.track("name_changed", { from: prev, to: next });
});
return null;
}
```
### `<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>
);
}
```
> **`<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.
### `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
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
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;
};
```
| 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
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
```
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.
`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
serializeSnapshot(snapshot, options?): string
serializeStore(store, options?): string
readHydration(id?: string): Record<string, unknown> | null
<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>;
}
// 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>;
}
```
### Open a modal from anywhere with `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.
```tsx
// atoms/modals.ts
import { portalAtom } from "@mongez/react-atom";
export const editUserModal = portalAtom<{ id: number; initialName: string }>("editUser");
```
```tsx
// components/UserRow.tsx — trigger
import { editUserModal } from "../atoms/modals";
function UserRow({ user }) {
return (
<tr>
<td>{user.name}</td>
<td>
<button onClick={() => editUserModal.open({ id: user.id, initialName: user.name })}>
Edit
</button>
</td>
</tr>
);
}
```
```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 onClose={() => editUserModal.close()}>
<h2>Edit user</h2>
<form onSubmit={async (e) => {
e.preventDefault();
await api.users.update(data.id, /* ... */);
editUserModal.close();
}}>
<input defaultValue={data.initialName} />
<button>Save</button>
</form>
</Dialog>
);
}
```
### Wire a paginated fetch with `fetchingAtom`
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
// atoms/products.ts
import { fetchingAtom } from "@mongez/react-atom";
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);
}
}
```
```tsx
// components/ProductsList.tsx
import { productsAtom, loadProducts } from "../atoms/products";
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>;
}
```
### Hydrate atoms in SSR (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
// 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";
import { currentUserAtom } from "@/atoms/user";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser(); // server-only round-trip
const store = createAtomStore();
store.use(currentUserAtom).update(user); // pre-fill the store
return (
<html lang="en">
<body>
<AtomStoreProvider store={store}>
{children}
<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 }) {
return (
<AtomStoreProvider initialValues={readHydration() ?? undefined}>
{children}
</AtomStoreProvider>
);
}
```
`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`.
### Call an action method safely under SSR
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.
```tsx
import { useAtom } from "@mongez/react-atom";
// 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);
return <button onClick={() => auth.logout()}>Log out</button>;
}
```
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.
### Isolate atom state per test
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.
```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";
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();
});
```
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.
### 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: "MyReactApp" });
}
```
Time-travel works: rewinding a `fetchingAtom` from `success` back to `startLoading` rolls the connected `UsersList` back to its spinner state.
> **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.
---
## TypeScript
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
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