Recoil vs Jotai in 2026: Atomic State Management Compared
TL;DR
Use Jotai. Recoil is effectively unmaintained. Recoil (~1.5M weekly downloads) was Meta's experiment in atomic state management — a good idea that solved a real problem, but Meta never made it a priority. The last major release was in 2023. Jotai (~3M downloads) has the same atomic model, active development, a thriving community, and a cleaner API. If you're starting a new project or currently on Recoil, migrate to Jotai.
Key Takeaways
- Jotai: ~3M weekly downloads — Recoil: ~1.5M (and declining)
- Recoil's last major version: 0.7 (2023) — development has stalled
- Jotai is smaller — ~3KB vs Recoil's ~21KB gzipped
- Both use the same atomic model — migration is mostly API renaming
- Jotai has active development — regular releases, growing ecosystem
The Recoil Problem
Recoil was introduced at React Europe 2020 and solved a genuine React problem: sharing state between components without lifting it to a common ancestor (prop drilling) or using a global store.
The atomic model was elegant. But Meta's internal priorities didn't align with open-source maintenance:
Recoil development history:
2020: Open sourced, great reception
2021: Active development, growing adoption
2022: Slowing releases, community concerns begin
2023: recoil@0.7 released, then silence
2024-2026: No major releases, issues pile up, React 18 bugs unfixed
The project isn't officially dead — just neglected.
API Comparison
// Recoil — requires RecoilRoot provider
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// 1. Must wrap app in RecoilRoot
function App() {
return <RecoilRoot><MyApp /></RecoilRoot>;
}
// 2. Define atoms (must have unique key)
const cartItemsAtom = atom({
key: 'cartItems', // Required and must be globally unique
default: [],
});
const cartTotalSelector = selector({
key: 'cartTotal', // Also must be globally unique
get: ({ get }) => {
const items = get(cartItemsAtom);
return items.reduce((sum, item) => sum + item.price, 0);
},
});
// 3. Use in components
function Cart() {
const [items, setItems] = useRecoilState(cartItemsAtom);
const total = useRecoilValue(cartTotalSelector);
return <div>Total: ${total} ({items.length} items)</div>;
}
// Jotai — no provider required (global store by default)
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// 1. No provider needed (works without it)
// 2. Define atoms (no key required)
const cartItemsAtom = atom([]);
const cartTotalAtom = atom(
(get) => get(cartItemsAtom).reduce((sum, item) => sum + item.price, 0)
);
// ↑ Derived atoms don't need a separate "selector" concept — just an atom with a getter
// 3. Use in components
function Cart() {
const [items] = useAtom(cartItemsAtom);
const total = useAtomValue(cartTotalAtom);
return <div>Total: ${total} ({items.length} items)</div>;
}
Migration from Recoil to Jotai
// Recoil → Jotai migration is mostly mechanical
// BEFORE (Recoil):
const textAtom = atom({
key: 'text',
default: '',
});
const upperCaseText = selector({
key: 'upperCaseText',
get: ({ get }) => get(textAtom).toUpperCase(),
});
// AFTER (Jotai):
const textAtom = atom(''); // Remove key and default wrapper
const upperCaseText = atom(get => get(textAtom).toUpperCase()); // selector → derived atom
// Hook changes:
// useRecoilState → useAtom
// useRecoilValue → useAtomValue
// useSetRecoilState → useSetAtom
// Provider changes:
// Remove <RecoilRoot> (not required in Jotai)
// OR replace with <Provider> for isolated stores
Jotai-Only Features
// Atom families (parameterized atoms)
import { atomFamily } from 'jotai/utils';
const todoAtomFamily = atomFamily(
(id) => atom({ id, text: '', completed: false })
);
function Todo({ id }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id));
return (
<input
value={todo.text}
onChange={e => setTodo(prev => ({ ...prev, text: e.target.value }))}
/>
);
}
// Atom with storage persistence
import { atomWithStorage } from 'jotai/utils';
const themeAtom = atomWithStorage('theme', 'light');
// Automatically persists to localStorage and syncs across tabs
// Async atoms with Suspense
const userAtom = atom(async () => {
const res = await fetch('/api/user');
return res.json();
});
function UserProfile() {
const user = useAtomValue(userAtom);
// Component suspends until atom resolves
return <div>{user.name}</div>;
}
// Wrap in Suspense:
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
Bundle Size
Recoil: ~21KB gzipped
Jotai: ~3KB gzipped
7x smaller — significant for performance-sensitive apps
When to Choose
Choose Jotai when:
- Starting any new project needing atomic state
- Currently on Recoil and facing React 18+ issues
- You want atomic state management that's actively maintained
- Bundle size matters
- You want async atoms with Suspense support
Stay on Recoil only when:
- Large codebase with hundreds of atoms — migration has real cost
- Everything works and you're not on React 18 concurrent features
- You have significant internal tooling built around Recoil
The verdict: Jotai is Recoil done right, with active maintenance and a smaller footprint.
Compare Recoil and Jotai package health on PkgPulse.
See the live comparison
View recoil vs. jotai on PkgPulse →