Skip to main content

Recoil vs Jotai in 2026: Atomic State Management Compared

·PkgPulse Team

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.