Recoil is an experimental state management framework for React applications that provides atoms and selectors for fine-grained reactivity.
—
Functions for creating parameterized atoms and selectors that are memoized by parameter. Family patterns allow you to create collections of related state that share the same structure but vary by some input parameter.
Creates a function that returns memoized atoms based on parameters.
/**
* Returns a function which returns a memoized atom for each unique parameter value
*/
function atomFamily<T, P extends SerializableParam>(
options: AtomFamilyOptions<T, P>
): (param: P) => RecoilState<T>;
type AtomFamilyOptions<T, P extends SerializableParam> = {
/** Unique string identifying this atom family */
key: string;
/** Default value or function that returns default based on parameter */
default?: T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> |
((param: P) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>);
/** Effects for each atom or function returning effects based on parameter */
effects?: ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
/** Allow direct mutation of atom values */
dangerouslyAllowMutability?: boolean;
};
type SerializableParam =
| undefined | null | boolean | number | symbol | string
| ReadonlyArray<SerializableParam>
| ReadonlySet<SerializableParam>
| ReadonlyMap<SerializableParam, SerializableParam>
| Readonly<{[key: string]: SerializableParam}>;Usage Examples:
import { atomFamily, useRecoilState } from 'recoil';
// Simple atom family with primitive parameter
const itemState = atomFamily({
key: 'itemState',
default: null,
});
// Usage in components
function ItemEditor({ itemId }) {
const [item, setItem] = useRecoilState(itemState(itemId));
return (
<input
value={item || ''}
onChange={(e) => setItem(e.target.value)}
/>
);
}
// Atom family with object parameter
const userPreferencesState = atomFamily({
key: 'userPreferencesState',
default: (userId) => ({
theme: 'light',
notifications: true,
userId,
}),
});
// Atom family with async default
const userProfileState = atomFamily({
key: 'userProfileState',
default: async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
// Atom family with parameter-based effects
const persistedItemState = atomFamily({
key: 'persistedItemState',
default: '',
effects: (itemId) => [
({setSelf, onSet}) => {
const key = `item-${itemId}`;
const saved = localStorage.getItem(key);
if (saved != null) {
setSelf(JSON.parse(saved));
}
onSet((newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
});
},
],
});Creates functions that return memoized selectors based on parameters.
/**
* Returns a function which returns a memoized selector for each unique parameter value
*/
function selectorFamily<T, P extends SerializableParam>(
options: ReadWriteSelectorFamilyOptions<T, P>
): (param: P) => RecoilState<T>;
function selectorFamily<T, P extends SerializableParam>(
options: ReadOnlySelectorFamilyOptions<T, P>
): (param: P) => RecoilValueReadOnly<T>;
interface ReadOnlySelectorFamilyOptions<T, P extends SerializableParam> {
/** Unique string identifying this selector family */
key: string;
/** Function that computes the selector's value based on parameter */
get: (param: P) => (opts: {
get: GetRecoilValue;
getCallback: GetCallback;
}) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
/** Cache policy for selectors in this family */
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
/** Allow direct mutation of selector values */
dangerouslyAllowMutability?: boolean;
}
interface ReadWriteSelectorFamilyOptions<T, P extends SerializableParam> {
/** Unique string identifying this selector family */
key: string;
/** Function that computes the selector's value based on parameter */
get: (param: P) => (opts: {
get: GetRecoilValue;
getCallback: GetCallback;
}) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
/** Function that handles setting the selector's value based on parameter */
set: (param: P) => (opts: {
set: SetRecoilState;
get: GetRecoilValue;
reset: ResetRecoilState;
}, newValue: T | DefaultValue) => void;
/** Cache policy for selectors in this family */
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
/** Allow direct mutation of selector values */
dangerouslyAllowMutability?: boolean;
}Usage Examples:
import { selectorFamily, atomFamily, useRecoilValue } from 'recoil';
// Read-only selector family
const itemWithMetadataState = selectorFamily({
key: 'itemWithMetadataState',
get: (itemId) => ({get}) => {
const item = get(itemState(itemId));
const metadata = get(itemMetadataState(itemId));
return {
...item,
...metadata,
fullId: `item-${itemId}`,
};
},
});
// Async selector family
const userPostsState = selectorFamily({
key: 'userPostsState',
get: (userId) => async ({get}) => {
const user = get(userState(userId));
const response = await fetch(`/api/users/${userId}/posts`);
return response.json();
},
});
// Read-write selector family
const itemDisplayNameState = selectorFamily({
key: 'itemDisplayNameState',
get: (itemId) => ({get}) => {
const item = get(itemState(itemId));
return item?.name || `Item ${itemId}`;
},
set: (itemId) => ({set, get}, newValue) => {
const currentItem = get(itemState(itemId));
set(itemState(itemId), {
...currentItem,
name: newValue as string,
});
},
});
// Selector family with complex parameter
const filteredListState = selectorFamily({
key: 'filteredListState',
get: ({listId, filter}) => ({get}) => {
const list = get(listState(listId));
return list.filter(item =>
item.category === filter.category &&
item.status === filter.status
);
},
});
// Usage with object parameter
function FilteredList({ listId, category, status }) {
const filteredItems = useRecoilValue(filteredListState({
listId,
filter: { category, status }
}));
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Examples of using effects with families for per-parameter side effects.
Usage Examples:
import { atomFamily } from 'recoil';
// WebSocket connection per chat room
const chatRoomState = atomFamily({
key: 'chatRoomState',
default: { messages: [], connected: false },
effects: (roomId) => [
({setSelf, onSet}) => {
let ws: WebSocket;
// Connect to room-specific WebSocket
const connect = () => {
ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);
ws.onopen = () => {
setSelf(current => ({ ...current, connected: true }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setSelf(current => ({
...current,
messages: [...current.messages, message],
}));
};
ws.onclose = () => {
setSelf(current => ({ ...current, connected: false }));
};
};
// Track when messages are sent
onSet((newValue, oldValue) => {
if (ws && ws.readyState === WebSocket.OPEN) {
const newMessages = newValue.messages;
const oldMessages = oldValue.messages || [];
if (newMessages.length > oldMessages.length) {
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.type === 'outgoing') {
ws.send(JSON.stringify(lastMessage));
}
}
}
});
connect();
// Cleanup
return () => {
if (ws) {
ws.close();
}
};
},
],
});
// API cache with per-endpoint invalidation
const apiCacheState = atomFamily({
key: 'apiCacheState',
default: null,
effects: (endpoint) => [
({setSelf}) => {
// Auto-refresh certain endpoints
if (endpoint.includes('/live-data/')) {
const interval = setInterval(async () => {
try {
const response = await fetch(endpoint);
const data = await response.json();
setSelf(data);
} catch (error) {
console.error(`Failed to refresh ${endpoint}:`, error);
}
}, 5000);
return () => clearInterval(interval);
}
},
],
});Parameter Design:
Performance Considerations:
Usage Examples:
import React, { useMemo } from 'react';
import { selectorFamily, useRecoilValue } from 'recoil';
// Good: Stable parameter object
function UserDashboard({ userId, filters }) {
const filterParams = useMemo(() => ({
userId,
...filters,
}), [userId, filters]);
const dashboardData = useRecoilValue(userDashboardState(filterParams));
return <div>{/* render dashboard */}</div>;
}
// Bad: New object on every render
function UserDashboardBad({ userId, filters }) {
// This creates a new parameter object on every render!
const dashboardData = useRecoilValue(userDashboardState({
userId,
...filters,
}));
return <div>{/* render dashboard */}</div>;
}Install with Tessl CLI
npx tessl i tessl/npm-recoil