Atomic State Management: Jotai & Recoil
As React applications grow, managing global state efficiently becomes a challenge. Traditional solutions like Redux rely on a single centralized store, which can sometimes lead to excessive boilerplate and performance issues related to unnecessary re-renders.
Atomic state management offers a different mental model. Tools like Recoil (developed at Facebook) and Jotai (a minimalist, bottom-up approach) treat state as independent, small, and isolated units called "atoms."
What is an Atom?
An atom represents a piece of state. You can read from and write to atoms from any component. When an atom is updated, only the components subscribed to that specific atom re-render. This fine-grained reactivity eliminates the need to carefully select specific slices of state to prevent performance bottlenecks.
Creating and Using Atoms
Here is how you define and use atoms in Jotai:
import { atom, useAtom } from 'jotai';
// 1. Define an atom outside the component
export const countAtom = atom(0);
function Counter() {
// 2. Use the atom inside the component (API is very similar to useState)
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
The syntax in Recoil is conceptually identical, though slightly more verbose as atoms require unique string keys:
import { atom, useRecoilState } from 'recoil';
export const countState = atom({
key: 'countState', // unique ID (with respect to other atoms/selectors)
default: 0, // default value
});
function Counter() {
const [count, setCount] = useRecoilState(countState);
// ...
}
Derived State (Selectors)
Often, you need state that is derived from one or more atoms. In atomic state management, this is handled by derived atoms (Jotai) or selectors (Recoil).
Derived state functions like a pure function: if the underlying atoms change, the derived state recalculates. If they haven't changed, the value is cached.
Derived State in Jotai
Jotai handles derived state simply by passing a read function to the atom constructor.
import { atom, useAtomValue } from 'jotai';
const priceAtom = atom(100);
const taxRateAtom = atom(0.2);
// Derived atom: computes total price based on price and tax rate
const totalPriceAtom = atom((get) => {
const price = get(priceAtom);
const tax = get(taxRateAtom);
return price + (price * tax);
});
function TotalDisplay() {
// useAtomValue reads the atom without providing a setter function
const total = useAtomValue(totalPriceAtom);
return <div>Total: ${total}</div>;
}
Selectors in Recoil
Recoil uses the selector function to define derived state.
import { selector, useRecoilValue } from 'recoil';
import { priceState, taxRateState } from './atoms';
const totalPriceState = selector({
key: 'totalPriceState',
get: ({ get }) => {
const price = get(priceState);
const tax = get(taxRateState);
return price + (price * tax);
},
});
function TotalDisplay() {
const total = useRecoilValue(totalPriceState);
return <div>Total: ${total}</div>;
}
Jotai vs. Recoil: Key Differences
While both implement the atomic pattern, they have distinct philosophies:
- API Surface & Size: Jotai is extremely minimalist and has a tiny bundle size. Recoil has a larger API surface, handling more complex graph traversal edge cases out of the box but comes with a larger bundle size.
- Keys: Recoil requires explicit string keys for every atom and selector. Jotai relies on object references, meaning you don't need to name them, avoiding key collision issues.
- Ecosystem: Recoil was developed at Facebook but has seen slower development momentum recently. Jotai is highly active, community-driven (part of the Poimandres collective), and supports excellent integrations (e.g., with React Query, Immer).
When to use Atomic State?
- When you have many small, isolated pieces of dynamic data.
- When components need to share state but are spread far apart in the React tree.
- When you are experiencing performance issues (unnecessary re-renders) with Context API or poorly optimized Redux stores.