React State Management in Depth #4: Jotai and the Atom Model
The Zustand of #3 was a top-down tool that gathers state into a single store. The Jotai of this post is the exact opposite. It breaks state into small units called atoms, then assembles those atoms to build larger state — a bottom-up model. It solves the same client-state problem from an entirely different angle, so studying the two side by side broadens your perspective on state management.
The idea of an atom #
Jotai’s starting point is the idea of “keeping state in fine-grained atomic units.” A single atom is the smallest piece of state, holding one value.
import { atom } from "jotai";
export const countAtom = atom(0);
export const nameAtom = atom("Guest");atom(0) defines “one atom whose initial value is 0.” What’s worth noting is that the atom itself holds no value. An atom is a kind of config, and the actual value is determined the moment a component subscribes to that atom. That’s why you can use the same atom in many places without conflict.
useAtom — using it like useState #
To use an atom in a component, you call useAtom. The shape of usage is almost identical to useState.
import { useAtom } from "jotai";
import { countAtom } from "./atoms";
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<button onClick={() => setCount((c) => c - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}The shape of returning a [value, setter] pair is exactly like useState. The difference is that this state is shared globally rather than being local to the component. Call useAtom on the same countAtom from another component and it sees the same value; change it on one side and both update together. Like Zustand in #3, Jotai also needs no Provider wrapping the tree for basic use.
If you only read or only write, use a narrower hook.
import { useAtomValue, useSetAtom } from "jotai";
const count = useAtomValue(countAtom); // read the value only
const setCount = useSetAtom(countAtom); // setter only (this component won't re-render on value changes)useSetAtom only writes the value and doesn’t subscribe, so the component won’t re-render even when the value changes.
Derived atoms — making atoms from atoms #
Jotai’s real power comes from derived atoms. You can define a new atom that reads the values of other atoms and computes from them.
import { atom } from "jotai";
export const countAtom = atom(0);
// a read-only derived atom that reads countAtom and computes its double
export const doubledAtom = atom((get) => get(countAtom) * 2);Passing a function as in atom((get) => ...) makes it a derived atom. You read another atom’s current value with get(countAtom). When countAtom changes, doubledAtom is recomputed automatically. It’s the same model as a spreadsheet, where changing one cell automatically updates the formulas that referenced it.
function DoubledView() {
const doubled = useAtomValue(doubledAtom);
return <p>Doubled: {doubled}</p>;
}You can also make a derived atom that combines several atoms.
const priceAtom = atom(1000);
const quantityAtom = atom(2);
const totalAtom = atom((get) => get(priceAtom) * get(quantityAtom));If either priceAtom or quantityAtom changes, totalAtom is recomputed. The derived values you used to compute by hand with useMemo in Basics #14 are managed for you by the atom graph in Jotai.
Re-renders are cut off per atom #
Jotai’s re-render model is simple. A component re-renders only when an atom it has useAtom’d (and the atoms that atom depends on) changes.
function NameInput() {
const [name, setName] = useAtom(nameAtom);
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
function CountDisplay() {
const count = useAtomValue(countAtom);
return <p>{count}</p>; // does not re-render when nameAtom changes
}Typing into nameAtom doesn’t redraw CountDisplay, which only watches countAtom. The precision you got from selectors in Zustand comes, in Jotai, from the very structure of having broken state into atoms from the start.
atomWithStorage — surviving a refresh #
Jotai’s counterpart to Zustand’s persist is atomWithStorage.
import { atomWithStorage } from "jotai/utils";
export const themeAtom = atomWithStorage("theme", "light");function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current: {theme}
</button>
);
}The first argument is the localStorage key, the second is the initial value. Use it like a normal atom, and changes are saved automatically and restored after a refresh.
Wrapping up #
Jotai is a bottom-up state management tool that breaks state into small atoms and assembles them.
atom(initialValue)— the smallest piece of state, no Provider neededuseAtom/useAtomValue/useSetAtom— usage resemblinguseState, but shared globally- derived atom
atom((get) => ...)— reads other atoms and computes; auto-updates when dependency atoms change - re-renders scoped per atom, for precision
atomWithStorage—localStoragesave and restore
That’s two lightweight client-state tools covered (Zustand, Jotai). In the next post, “React State Management in Depth #5: Redux Toolkit and the Legacy Context,” we turn to Redux — which ruled an era and still lives in many codebases — in its currently recommended form, Redux Toolkit. We’ll also place where it stands in a new project.