Jotai and Recoil are both fantastic libraries for atomic state management in React, but they tackle similar problems with slightly different philosophies, leading to distinct sweet spots.

Let’s look at Jotai first. Imagine you have a simple counter. In Jotai, you’d define that as an atom:

import { atom } from 'jotai';

export const counterAtom = atom(0);

Now, in your React component, you can use useAtom to read and write to it:

import { useAtom } from 'jotai';
import { counterAtom } from './atoms';

function Counter() {
  const [count, setCount] = useAtom(counterAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

This is incredibly direct. No context providers wrapping your entire app, no complex setup. Jotai’s magic is that it is the context provider, implicitly. When you use an atom, Jotai finds the nearest provider or creates one if needed. This makes it incredibly lightweight and scalable for applications where you might have many independent pieces of state scattered throughout your component tree. It feels like useState, but global.

Recoil, on the other hand, often feels more like a framework for state management. You typically define your atoms within a RecoilRoot provider:

import { RecoilRoot, atom } from 'recoil';

const counterState = atom({
  key: 'counterState',
  default: 0,
});

function App() {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
}

function Counter() {
  const [count, setCount] = useRecoilState(counterState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

The key is a crucial differentiator. Recoil uses these keys for serialization, debugging, and internal tracking. This gives Recoil a more structured feel, especially when dealing with complex state graphs or when you need to persist state across sessions. Recoil’s emphasis on "selectors" (derived state) is also a powerful feature, allowing you to compute new state based on existing atoms in a memoized way.

So, when do you choose which?

Use Jotai when:

  • You want minimal boilerplate and a familiar useState-like API for global state. Jotai excels at making global state feel as easy to manage as local state. If your primary concern is quickly adding shared state without disrupting your component structure or adding many context providers, Jotai is your champion.
  • You’re building a library or component that needs to manage its own state independently. Jotai’s implicit provider mechanism means your component can use atoms without forcing its consumers to wrap them in a specific provider.
  • You have many small, independent pieces of state. Jotai’s atom-centric approach scales beautifully for applications with numerous, loosely coupled state slices.
  • Performance with fine-grained updates is paramount. Jotai’s design allows for very precise re-renders, only updating components that truly depend on the changed atom.

Use Recoil when:

  • You need robust debugging and state serialization capabilities. Recoil’s key system and built-in devtools make it easier to inspect, debug, and persist state.
  • You have complex derived state logic. Recoil’s selectors are exceptionally powerful for computing values based on other atoms, with built-in memoization. If you find yourself writing a lot of useMemo or complex logic to derive state from other state, Recoil’s selectors might be a more elegant solution.
  • You prefer a more structured, framework-like approach to state management. Recoil provides a clear pattern for defining state, derived state, and side effects.
  • You’re building a large, monolithic application where a single RecoilRoot at the top is acceptable and beneficial for centralized management.

Consider an application with a user authentication status and a shopping cart.

With Jotai:

You might have const authAtom = atom(null); and const cartItemsAtom = atom([]);. Components can subscribe to either directly:

// AuthStatus.js
import { useAtom } from 'jotai';
import { authAtom } from './atoms';

function AuthStatus() {
  const [user] = useAtom(authAtom);
  return <div>{user ? `Welcome, ${user.name}` : 'Please log in'}</div>;
}

// Cart.js
import { useAtom } from 'jotai';
import { cartItemsAtom } from './atoms';

function Cart() {
  const [items] = useAtom(cartItemsAtom);
  return <div>Cart has {items.length} items.</div>;
}

No explicit providers needed in App.js for these atoms.

With Recoil:

You’d define them in atoms, likely in a central state/index.js:

// state/atoms.js
import { atom } from 'recoil';

export const authState = atom({
  key: 'authState',
  default: null,
});

export const cartItemsState = atom({
  key: 'cartItemsState',
  default: [],
});

And then use them within a RecoilRoot:

// App.js
import { RecoilRoot } from 'recoil';
import AuthStatus from './AuthStatus';
import Cart from './Cart';

function App() {
  return (
    <RecoilRoot>
      <AuthStatus />
      <Cart />
      {/* ... other components */}
    </RecoilRoot>
  );
}

// AuthStatus.js
import { useRecoilValue } from 'recoil';
import { authState } from '../state/atoms';

function AuthStatus() {
  const user = useRecoilValue(authState); // useRecoilValue for read-only
  return <div>{user ? `Welcome, ${user.name}` : 'Please log in'}</div>;
}

// Cart.js
import { useRecoilValue } from 'recoil';
import { cartItemsState } from '../state/atoms';

function Cart() {
  const items = useRecoilValue(cartItemsState);
  return <div>Cart has {items.length} items.</div>;
}

The one thing that often trips people up with Jotai is its implicit nature. You might reach for useAtom and forget that the atom itself is the source of truth, not a component. It’s not that you must define atoms in a top-level atoms.js file, but doing so helps with organization, especially as your application grows, preventing the "where did this atom come from?" confusion. Jotai’s power lies in its ability to seamlessly integrate into any part of your tree without requiring explicit prop drilling or context setup for each individual atom, making it feel very organic.

Ultimately, the choice often comes down to your project’s complexity, your team’s preferences for API style, and the importance of features like advanced debugging or complex derived state management. Both are excellent tools for solving the problem of global state in React.

You’ll likely encounter the need for derived state next, and both libraries offer robust solutions for it.

Want structured learning?

Take the full React course →