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 aLoadablewith astateof'hasValue','loading', or'hasError'plus acontentsfield.
How the hooks compare
| Hook | Returns | Use when |
|---|---|---|
useRecoilState(atom) | [value, setter] | A component reads and writes the same atom. |
useRecoilValue(node) | value | Read-only access to an atom or selector. |
useSetRecoilState(atom) | setter | Write-only; avoids re-rendering on changes. |
useResetRecoilState(atom) | reset() | Restore an atom to its default. |
useRecoilValueLoadable(node) | Loadable | Handle 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
useRecoilValueand write withuseSetRecoilStateto avoid needless re-renders. - Wrap components that read async selectors in a
<Suspense>boundary, or useuseRecoilValueLoadablefor 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.