Atomic state management with auto-dependency tracking
Quick Guide: Jotai builds state bottom-up out of atoms, each tracking its own dependencies, so a component re-renders only for the atoms it read. Atoms are defined at module level; defining one inside a component makes a new atom on every render and the state never survives. Async atoms suspend by default, so their consumers need a Suspense boundary —
loadable()andunwrap()are the ways out of that.atomFamilyfromjotai/utilsis deprecated in favour of thejotai-familypackage.
Detailed Resources:
loadable, unwrap, atom familiesatomWithStorage, selectAtom, splitAtom<critical_requirements>
Define atoms at module level. An atom is an identity, not a value — one created during render is a new identity each time, so the state resets on every render and the component appears not to respond.
Give async atom consumers a Suspense boundary. An async atom suspends the moment it is read, and a suspension with nothing to catch it propagates as an error.
Put multi-atom updates behind a write-only atom. The updates then land together, and the logic is reachable from anywhere without a component around it.
Use the jotai-family package for parameterised atoms. atomFamily in jotai/utils is deprecated, and families of either kind hold every atom they create until told otherwise.
</critical_requirements>
Auto-detection: atom, useAtom, useAtomValue, useSetAtom, atomWithStorage, atomWithReset, splitAtom, selectAtom, loadable, unwrap, createStore, RESET, jotai/utils, jotai-family, Provider store
Applies to:
Handled elsewhere:
Atoms are cells in a spreadsheet. A primitive atom holds a value, a derived atom is a formula over other cells, and changing a cell recalculates exactly the formulas that read it — nothing declares a dependency, because the dependency is whatever get was called on while the formula ran.
Two things follow. State is built bottom-up from small pieces rather than carved out of one large object, so the re-render boundary comes for free instead of from memoisation. And an atom is an identity, not a container: the value lives in a store, and the atom is the key. That is why the same atom read under two Providers gives two values, and why an atom created inside render is a different key each time.
</philosophy><decision_framework>
Does it hold a value of its own?
├─ YES -> primitive: atom(initialValue)
└─ NO -> Is it computed from other atoms?
├─ YES -> Does it also accept writes?
│ ├─ YES -> read-write: atom(read, write)
│ └─ NO -> derived: atom((get) => ...)
└─ NO -> it only performs updates
└─ write-only: atom(null, (get, set) => ...)
Should the consumer suspend while the value is pending?
├─ YES -> read the async atom directly, under a Suspense boundary
└─ NO -> Does the UI need to distinguish loading from error?
├─ YES -> loadable(asyncAtom) — a discriminated union on `state`
└─ NO -> unwrap(asyncAtom, fallback) — a plain value throughout
Does each item update independently?
├─ YES -> splitAtom, with a keyExtractor where items have ids
└─ NO -> a single array atom is enough
Reach for a derived atom before selectAtom. The official docs call selectAtom an escape hatch; a derived atom expresses the same read with the dependency tracking that is the point of the library.
</decision_framework>
A single value, typed by inference. Explicit types are for unions and nullables.
import { atom } from "jotai";
const countAtom = atom(0);
const userAtom = atom<User | null>(null);
const themeAtom = atom<"light" | "dark" | "system">("light");
Full code: examples/core.md
Dependencies are whatever the read function called get on, re-derived on each evaluation and cached until one of them changes.
const subtotalAtom = atom((get) => get(priceAtom) * get(quantityAtom));
const taxAtom = atom((get) => get(subtotalAtom) * get(taxRateAtom));
const totalAtom = atom((get) => get(subtotalAtom) + get(taxAtom));
Full code: examples/core.md
A null read function marks an atom that only performs updates. Every set inside one write lands together.
const resetAllAtom = atom(null, (get, set) => {
set(countAtom, 0);
set(itemsAtom, []);
set(selectedAtom, null);
});
Full code: examples/core.md
A lens onto part of a larger value: the read narrows, the write puts the whole value back.
const nameAtom = atom(
(get) => get(userAtom).name,
(get, set, newName: string) => {
set(userAtom, { ...get(userAtom), name: newName });
},
);
Full code: examples/core.md
An async read function makes the atom suspend when read.
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<User>;
});
// Or, to handle the states by hand:
const loadableUserAtom = loadable(userAtom);
// { state: "loading" } | { state: "hasData", data } | { state: "hasError", error }
Full code: examples/async.md
Backs an atom with localStorage, sessionStorage or a storage object of your own. Setting it to RESET restores the initial value.
import { atomWithStorage, RESET } from "jotai/utils";
const themeAtom = atomWithStorage<Theme>("app-theme", "light");
By default the first render shows the initial value and the stored value arrives after, which flickers. { getOnInit: true } reads storage immediately, at the cost of a server render that cannot agree with it.
Full code: examples/persistence.md
Turns an array atom into an atom of item atoms, so editing one item re-renders one row.
import { splitAtom } from "jotai/utils";
const todosAtom = atom<Todo[]>([]);
const todoAtomsAtom = splitAtom(todosAtom, (todo) => todo.id);
The key extractor is what keeps an item's atom identity stable across a reorder.
Full code: examples/persistence.md
A store is where atom values actually live. Creating one explicitly gives access outside React, and isolation between trees.
import { createStore, Provider } from "jotai";
const store = createStore();
store.set(countAtom, 10);
store.sub(countAtom, () => {
/* value changed */
});
<Provider store={store}>
<App />
</Provider>;
Full code: examples/testing.md
</patterns><red_flags>
Breaks at runtime:
useMemo over that prop is the narrow exception.atomFamily from jotai/utils — deprecated; jotai-family is the current package.remove() or setShouldRemove() is what bounds it.Surprising behaviour:
Provider holds its own values, so the same atom under two Providers is two pieces of state. Passing one store to both is what shares them.atomWithStorage renders the initial value first and the stored value after, so a persisted theme flashes the default. getOnInit: true fixes the flash and introduces a hydration mismatch wherever the server rendered the default.loadable() never throws and never suspends; it returns a union, so reading .data without checking .state is reading a field that is not always there.</red_flags>
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer