Skip to content
React rc state-management 4 min read

Recoil

Recoil is an experimental state-management library from Meta that models shared state as a graph of small, independent units called atoms, with selectors computing derived values on top of them. Components subscribe to exactly the atoms or selectors they read, so updates re-render only the parts of the tree that actually depend on the changed state. It was designed alongside React’s concurrent rendering, with a hooks-first API that feels like a natural extension of useState. Before adopting it, though, it is worth knowing where the project stands today (see the maintenance note below).

Setting up RecoilRoot

All Recoil state lives inside a <RecoilRoot> context. Wrap your app once near the top of the tree — any component beneath it can read and write atoms.

// main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </StrictMode>
);

Install it alongside React:

npm install recoil

Atoms: units of state

An atom is a single piece of state. You give it a globally unique key and a default value. Any component that reads the atom subscribes to it, and writing to it re-renders only those subscribers.

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

export const counterState = atom({
  key: 'counterState', // must be unique across the whole app
  default: 0,
});

Read and write an atom with useRecoilState, which returns a [value, setValue] tuple exactly like useState.

// Counter.jsx
import { useRecoilState } from 'recoil';
import { counterState } from './state/counter';

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

  return (
    <section>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </section>
  );
}

When you only need to read, use useRecoilValue; when you only need to write, use useSetRecoilState. A write-only component never re-renders when the atom changes.

import { useRecoilValue, useSetRecoilState } from 'recoil';

function Display() {
  const count = useRecoilValue(counterState); // re-renders on change
  return <p>The count is {count}.</p>;
}

function ResetButton() {
  const setCount = useSetRecoilState(counterState); // never re-renders
  return <button onClick={() => setCount(0)}>Reset</button>;
}

Selectors: derived state

A selector computes a value from atoms (or other selectors) through a pure get function. Recoil caches the result and only recomputes when an upstream dependency changes, so derived values stay consistent without manual memoization.

// state/counter.js
import { atom, selector } from 'recoil';

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

export const doubledState = selector({
  key: 'doubledState',
  get: ({ get }) => get(counterState) * 2,
});
import { useRecoilValue } from 'recoil';
import { doubledState } from './state/counter';

function Doubled() {
  const doubled = useRecoilValue(doubledState);
  return <p>Doubled: {doubled}</p>;
}

Selectors can also be writable by adding a set function, letting you funnel writes back to underlying atoms:

export const tempCelsius = atom({ key: 'tempCelsius', default: 20 });

export const tempFahrenheit = selector({
  key: 'tempFahrenheit',
  get: ({ get }) => get(tempCelsius) * 1.8 + 32,
  set: ({ set }, newF) => set(tempCelsius, (newF - 32) / 1.8),
});

Async selectors

Selectors may return a Promise. Recoil treats the pending Promise as a React Suspense boundary trigger, so you render loading UI declaratively instead of juggling loading flags. This is the part of Recoil that leans hardest on concurrent rendering.

import { selector, useRecoilValue } from 'recoil';
import { Suspense } from 'react';

const userQuery = selector({
  key: 'userQuery',
  get: async () => {
    const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
    return res.json();
  },
});

function UserName() {
  const user = useRecoilValue(userQuery);
  return <p>Signed in as {user.name}</p>;
}

export default function Profile() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <UserName />
    </Suspense>
  );
}

After the request resolves, the component renders:

Output:

Signed in as Leanne Graham

If you would rather handle pending and error states inline instead of with Suspense, use useRecoilValueLoadable. It returns a Loadable with a state of 'hasValue', 'loading', or 'hasError' plus a contents field.

How the hooks compare

HookReturnsUse when
useRecoilState(atom)[value, setter]A component reads and writes the same atom.
useRecoilValue(node)valueRead-only access to an atom or selector.
useSetRecoilState(atom)setterWrite-only; avoids re-rendering on changes.
useResetRecoilState(atom)reset()Restore an atom to its default.
useRecoilValueLoadable(node)LoadableHandle async selector states without Suspense.

Maintenance status

This is the most important practical caveat: Recoil is effectively unmaintained. The library never reached a stable 1.0, its last release was in 2023, and Meta wound down the team behind it. For new projects, the React community has largely moved to Jotai — which offers the same atom-based mental model with a smaller, actively maintained codebase — or to Zustand for a store-based approach. Treat the examples here as conceptually valuable, but prefer a maintained alternative for production code.

Best Practices

  • Give every atom and selector a unique, descriptive key; duplicate keys throw at startup.
  • Keep selectors pure — never mutate state or trigger side effects inside get.
  • Read with useRecoilValue and write with useSetRecoilState to avoid needless re-renders.
  • Wrap components that read async selectors in a <Suspense> boundary, or use useRecoilValueLoadable for inline handling.
  • Co-locate related atoms and selectors in dedicated state modules rather than scattering them across components.
  • For new applications, evaluate Jotai or Zustand first given Recoil’s stalled maintenance.
Last updated June 14, 2026
Was this helpful?