patterntypescriptreactTip
Recoil selectors — derived and async computed state
Viewed 0 times
recoilselectorderived stateasync selectoratommemoisedSuspense integration
Error Messages
Problem
Recoil atoms hold raw state. Computing derived values (filtered lists, aggregates, async lookups) inside components duplicates logic and defeats the purpose of a centralised state graph. Selectors are the correct tool for derived and asynchronous state in Recoil.
Solution
Define synchronous and async selectors derived from atoms:
import { atom, selector, useRecoilValue, useRecoilState } from 'recoil';
const todoListAtom = atom<{ id: number; text: string; done: boolean }[]>({
key: 'todoList',
default: [],
});
const filterAtom = atom<'all' | 'done' | 'pending'>({
key: 'filter',
default: 'all',
});
// Synchronous derived selector
const filteredTodosSelector = selector({
key: 'filteredTodos',
get: ({ get }) => {
const todos = get(todoListAtom);
const filter = get(filterAtom);
if (filter === 'done') return todos.filter((t) => t.done);
if (filter === 'pending') return todos.filter((t) => !t.done);
return todos;
},
});
// Async selector (wraps a suspense boundary)
const userSelector = selector({
key: 'currentUser',
get: async () => {
const res = await fetch('/api/me');
return res.json();
},
});
function FilteredList() {
const todos = useRecoilValue(filteredTodosSelector);
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
import { atom, selector, useRecoilValue, useRecoilState } from 'recoil';
const todoListAtom = atom<{ id: number; text: string; done: boolean }[]>({
key: 'todoList',
default: [],
});
const filterAtom = atom<'all' | 'done' | 'pending'>({
key: 'filter',
default: 'all',
});
// Synchronous derived selector
const filteredTodosSelector = selector({
key: 'filteredTodos',
get: ({ get }) => {
const todos = get(todoListAtom);
const filter = get(filterAtom);
if (filter === 'done') return todos.filter((t) => t.done);
if (filter === 'pending') return todos.filter((t) => !t.done);
return todos;
},
});
// Async selector (wraps a suspense boundary)
const userSelector = selector({
key: 'currentUser',
get: async () => {
const res = await fetch('/api/me');
return res.json();
},
});
function FilteredList() {
const todos = useRecoilValue(filteredTodosSelector);
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
Why
Selectors are memoised — they only recompute when their dependencies (atoms or other selectors) change. Async selectors integrate with React Suspense, letting components suspend while data loads without manual loading state.
Gotchas
- Each atom and selector key must be globally unique — duplicate keys cause runtime errors
- Async selectors require a Suspense boundary in the component tree
- Selectors are read-only by default — use writable selectors (with set) to propagate writes back to atoms
- Recoil is in maintenance mode (Meta paused active development) — consider Jotai as an actively maintained alternative with a similar model
Revisions (0)
No revisions yet.