Recoil
Facebook's experimental state management library with atoms and selectors
Recoil - React State Management by Meta
Recoil is an experimental state management library from Meta (Facebook). It was designed specifically for React and provides a more React-like approach to global state with atoms and selectors. While still experimental, it's used in production at Meta and offers powerful features for complex state management.
⚠️ Note
Recoil is still marked as experimental by Meta. For new projects, consider Jotai as an alternative with similar atomic concepts but more active development.
Core Concepts
- Atoms — Units of state that components can subscribe to
- Selectors — Derived state from atoms or other selectors
- RecoilRoot — Provider component wrapping your app
- Async Selectors — Handle async data with Suspense
Installation
npm install recoil
Setup and Basic Atoms
import { RecoilRoot, atom, useRecoilState, useRecoilValue } from 'recoil';
// Create atoms
const textState = atom({
key: 'textState', // unique ID (required)
default: '', // default value
});
const countState = atom({
key: 'countState',
default: 0,
});
// Wrap your app with RecoilRoot
function App() {
return (
<RecoilRoot>
<TextInput />
<CharacterCount />
<Counter />
</RecoilRoot>
);
}
// Use atoms in components
function TextInput() {
const [text, setText] = useRecoilState(textState);
return (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
);
}
// Read-only access
function Counter() {
const count = useRecoilValue(countState);
return <span>Count: {count}</span>;
}
Selectors - Derived State
import { selector, useRecoilValue } from 'recoil';
// Selector derived from atom
const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
// Multiple dependencies
const todoStatsState = selector({
key: 'todoStatsState',
get: ({ get }) => {
const todos = get(todoListState);
const totalNum = todos.length;
const completedNum = todos.filter(t => t.completed).length;
const uncompletedNum = totalNum - completedNum;
const percentComplete = totalNum === 0 ? 0 : Math.round((completedNum / totalNum) * 100);
return {
totalNum,
completedNum,
uncompletedNum,
percentComplete,
};
},
});
function TodoStats() {
const { totalNum, completedNum, percentComplete } = useRecoilValue(todoStatsState);
return (
<div>
<p>Total: {totalNum}</p>
<p>Completed: {completedNum}</p>
<p>Progress: {percentComplete}%</p>
</div>
);
}
Writeable Selectors
import { selector, useRecoilState } from 'recoil';
const celsiusState = atom({
key: 'celsiusState',
default: 25,
});
const fahrenheitState = selector({
key: 'fahrenheitState',
get: ({ get }) => get(celsiusState) * 9/5 + 32,
set: ({ set }, newValue) => {
const celsius = (newValue - 32) * 5/9;
set(celsiusState, celsius);
},
});
function TemperatureConverter() {
const [celsius, setCelsius] = useRecoilState(celsiusState);
const [fahrenheit, setFahrenheit] = useRecoilState(fahrenheitState);
return (
<div>
<input
type="number"
value={celsius}
onChange={(e) => setCelsius(Number(e.target.value))}
/>°C
<input
type="number"
value={fahrenheit}
onChange={(e) => setFahrenheit(Number(e.target.value))}
/>°F
</div>
);
}
Async Selectors
import { selector, useRecoilValue } from 'recoil';
import { Suspense } from 'react';
const userIdState = atom({
key: 'userIdState',
default: 1,
});
// Async selector - automatically integrates with Suspense
const currentUserState = selector({
key: 'currentUserState',
get: async ({ get }) => {
const userId = get(userIdState);
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
const userPostsState = selector({
key: 'userPostsState',
get: async ({ get }) => {
const user = await get(currentUserState);
const response = await fetch(`/api/users/${user.id}/posts`);
return response.json();
},
});
function UserProfile() {
const user = useRecoilValue(currentUserState);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<RecoilRoot>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
</RecoilRoot>
);
}
Atom Family - Dynamic Atoms
import { atomFamily, selectorFamily, useRecoilState } from 'recoil';
// Create atoms dynamically with parameters
const todoItemState = atomFamily({
key: 'todoItemState',
default: (id) => ({
id,
text: '',
completed: false,
}),
});
// Selector family for parameterized selectors
const userByIdState = selectorFamily({
key: 'userByIdState',
get: (userId) => async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
function TodoItem({ id }) {
const [item, setItem] = useRecoilState(todoItemState(id));
return (
<div>
<input
type="checkbox"
checked={item.completed}
onChange={() => setItem({ ...item, completed: !item.completed })}
/>
<span>{item.text}</span>
</div>
);
}
function UserCard({ userId }) {
const user = useRecoilValue(userByIdState(userId));
return <div>{user.name}</div>;
}
Persistence with Effects
import { atom, AtomEffect } from 'recoil';
// Effect for localStorage persistence
const localStorageEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
// Use effect in atom
const userPrefsState = atom({
key: 'userPrefsState',
default: {
theme: 'light',
language: 'en',
},
effects: [
localStorageEffect('user_preferences'),
],
});
Recoil vs Jotai
| Feature | Recoil | Jotai |
|---|---|---|
| Provider | Required (RecoilRoot) | Optional |
| Atom keys | Required (strings) | Not needed |
| Bundle size | ~20KB | ~3KB |
| Status | Experimental | Stable |
| Maintainer | Meta | Poimandres (Pmndrs) |
💡 Key Takeaways
- • Recoil uses atoms and selectors similar to Jotai
- • Requires unique string keys for all atoms/selectors
- • Great Suspense integration for async data
- • Atom/selector families for dynamic state
- • Consider Jotai for new projects (smaller, more active)