Jotai (Japanese for "state") is a primitive and flexible state management library for React. It takes an atomic approach: state is split into small, independent pieces called atoms, and components can read or write them directly. Unlike Redux or Zustand, there’s no central store, no reducers, and no boilerplate. You define atoms, use them in components, and compose them to derive new state. The result is a minimal API surface, excellent TypeScript support, and seamless React Concurrent Mode compatibility.
Why Jotai Matters
Minimal mental overhead: You create an atom, read it, set it. That's it.
Automatic dependency tracking: Derived atoms re-evaluate only when their dependencies change, eliminating unnecessary renders.
Composable and scalable: Atoms can be combined like Lego bricks to form complex state graphs without monolithic stores.
First-class async support: Suspense and ErrorBoundary integrate naturally for loading and error states.
Zero configuration: No context providers needed for basic usage; the default store works globally.
Getting Started: The Basics
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
An atom represents a piece of state. Create one with atom(initialValue). To read and write it inside a component, use the useAtom hook, which returns a tuple just like useState.
Atoms can depend on other atoms. A derived atom is defined by passing a read function (and optionally a write function) to atom(). The read function receives get, which can read other atoms, and the write function receives get and set.
Read-only derived atom: Only a read function.
Write-only atom: Only a write function (often used for actions).
Read-write atom: Both read and write.
// Read-only: doubles the counter
const doubleCountAtom = atom((get) => get(counterAtom) * 2);
// Component using read-only atom
function DoubleDisplay() {
const [doubleCount] = useAtom(doubleCountAtom); // only reads
return
Double: {doubleCount}
;
}
// Write-only: increments the counter by a specific amount
const incrementByAtom = atom(null, (get, set, amount: number) => {
set(counterAtom, get(counterAtom) + amount);
});
function IncrementButton({ amount }: { amount: number }) {
const [, increment] = useAtom(incrementByAtom);
return ;
}
// Read-write: a controlled input that syncs with a local atom but also updates a parent atom
const textAtom = atom('');
const uppercaseAtom = atom(
(get) => get(textAtom).toUpperCase(),
(get, set, newValue: string) => set(textAtom, newValue.toLowerCase())
);
Understanding Atoms in Depth
Atom Configuration
The atom function can accept:
A single value → primitive atom.
A read function (get) => value → derived read-only atom.
A read function and a write function → read-write atom.
An object with read and write properties (legacy syntax).
Inside the write function, you have access to set, which can update any atom. The third argument is the value passed when calling the setter. You can also get to read the current value of any atom, including the atom being written, to support updater patterns.
Derived atoms can return promises. When a component reads an async atom, React's Suspense will catch the promise and show a fallback until it resolves. This eliminates manual loading state management.
;
}
// Wrap with Suspense
function App() {
return (
Loading user...
}>
);
}
For error handling, wrap with an ErrorBoundary (using libraries like react-error-boundary). Jotai also provides a useAtomCallback or loadable utilities for more granular control, but Suspense is the idiomatic way.
Atom Types and Serializability
Atoms can hold any value: primitives, objects, arrays, Maps, Sets, functions, or promises. However, for debugging and time-travel features, keeping atoms serializable is recommended. Jotai does not enforce immutability by default—you can mutate objects if you wish, but to trigger re-renders you must call set with a new reference. For complex structures, consider using splitAtom or libraries like Immer.
Advanced Patterns: Atom Families and Utilities
Atom Families
When you need multiple instances of the same atom pattern (e.g., a counter per item), use atomFamily. It takes a factory function and returns a function that creates or retrieves an atom for a given key.
import { atomFamily } from 'jotai/utils';
const countFamily = atomFamily((id: string) => atom(0));
function ItemCounter({ id }: { id: string }) {
const [count, setCount] = useAtom(countFamily(id));
return (
Item {id}: {count}
);
}
The atom is created lazily on first access and cached. You can also provide a custom equality function to control when the atom is recreated. The atomFamily supports a second parameter: areEqual or shouldRemove.
Optimized Hooks: useAtomValue and useSetAtom
To prevent unnecessary re-renders when a component only needs to read or write an atom, use useAtomValue and useSetAtom. These hooks don't subscribe to the whole atom tuple, reducing rendering overhead.
import { useAtomValue, useSetAtom } from 'jotai';
const todosAtom = atom([]);
// Component that only displays count, not the array
function TodoCount() {
const count = useAtomValue(todosAtom).length; // re-renders only when length changes? Actually useAtomValue subscribes to atom.
// Better: use a derived atom for count
return {count} items;
}
// Component that only adds a todo
function AddTodo() {
const setTodos = useSetAtom(todosAtom);
const add = (text: string) => {
setTodos((prev) => [...prev, text]);
};
// ...
}
Note: useAtomValue still re-renders when the atom value changes. For truly granular subscriptions, split the atom or use selectAtom.
selectAtom and splitAtom
selectAtom lets you derive a piece of a larger atom and only re-render when that piece changes.
import { selectAtom } from 'jotai/utils';
const bigObjectAtom = atom({ name: 'Alice', age: 30, city: 'NYC' });
const nameAtom = selectAtom(bigObjectAtom, (obj) => obj.name);
// This component re-renders only when name changes
function NameDisplay() {
const name = useAtomValue(nameAtom);
return
{name}
;
}
splitAtom takes an array atom and returns a dynamic list of atoms for each element, perfect for editable lists.
When server-rendering, you may want to seed atoms with initial data. useHydrateAtoms allows you to hydrate atoms from outside (e.g., from props or a global store).
By default, Jotai uses a global store. For testing or isolating state to a component subtree, you can use Provider to create a scoped store. All atoms used inside that Provider will be independent from the global store.
import { Provider, atom, useAtom } from 'jotai';
const themeAtom = atom('light');
function ThemedApp() {
return (
);
}
This is especially useful in component libraries where you want to avoid leaking state.
Jotai with Next.js and React Native
Jotai works out of the box with Next.js App Router and Pages Router. For server components, you can create atoms that fetch data on the server, then pass initial values to client components via useHydrateAtoms. In React Native, Jotai has zero dependencies on DOM APIs and works seamlessly.
DevTools and Debugging
Jotai provides an official devtools package (jotai-devtools) that visualizes the atom dependency graph, allows time-travel debugging, and inspects atom values in real time. Install and wrap your app with the DevTools provider.
import { DevTools } from 'jotai-devtools';
function App() {
return (
{/* your app */}
);
}
Best Practices and Patterns
Keep Atoms Small and Focused
Avoid large atoms that hold the entire application state. Instead, decompose state into primitive atoms and compose them with derived atoms. This minimizes re-renders and improves code clarity.
Prefer Derived Atoms Over Effect Hooks
Instead of using useEffect to synchronize state, derive values declaratively with atom. This avoids glitches and stale closures.
Avoid Unnecessary Re-renders
Use useAtomValue if you only read, useSetAtom if you only write.
Split large atoms into smaller ones, or use selectAtom.
Use atomFamily for lists to isolate updates to individual items.
Handling Side Effects
Atoms themselves don't support side effects directly, but you can create a write-only atom that triggers an effect externally. For complex workflows, combine Jotai with useEffect or libraries like jotai-effect.
Jotai encourages colocation: define atoms in the same file or folder as the component that uses them. This improves modularity and avoids large, central state files. For shared atoms, create a small atoms.ts in the feature folder.
Testing Atoms
Atoms are pure functions; test them outside React by calling get and set directly on a store. Use createStore from Jotai to get a standalone store instance.
import { createStore, atom } from 'jotai';
const store = createStore();
const countAtom = atom(0);
store.set(countAtom, 5);
expect(store.get(countAtom)).toBe(5);
For components, wrap them with Provider using a custom store to isolate tests.
Migration from Redux or Recoil
Map Redux slices to atoms: each slice becomes a primitive atom with write functions. Use derived atoms for selectors. For Recoil, atoms and selectors map almost 1:1, but Jotai's API is simpler and doesn't require key strings. Migrate incrementally: you can run Jotai alongside Redux using a Provider.
Conclusion
Jotai takes you from simple counters to complex, async, and highly dynamic state architectures without ever forcing a rigid structure. Its atomic model encourages you to think in small, composable pieces, resulting in cleaner, more performant React applications. By mastering the basics, derived atoms, async patterns, and utilities like atomFamily and selectAtom, you can handle any state management challenge while keeping your codebase maintainable and testable. Start small, compose boldly, and let Jotai do the heavy lifting.
🚀 Need a reliable AI agent for your project?
Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.