0
# Family Patterns
1
2
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.
3
4
## Capabilities
5
6
### Atom Families
7
8
Creates a function that returns memoized atoms based on parameters.
9
10
```typescript { .api }
11
/**
12
* Returns a function which returns a memoized atom for each unique parameter value
13
*/
14
function atomFamily<T, P extends SerializableParam>(
15
options: AtomFamilyOptions<T, P>
16
): (param: P) => RecoilState<T>;
17
18
type AtomFamilyOptions<T, P extends SerializableParam> = {
19
/** Unique string identifying this atom family */
20
key: string;
21
/** Default value or function that returns default based on parameter */
22
default?: T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> |
23
((param: P) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>);
24
/** Effects for each atom or function returning effects based on parameter */
25
effects?: ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
26
/** Allow direct mutation of atom values */
27
dangerouslyAllowMutability?: boolean;
28
};
29
30
type SerializableParam =
31
| undefined | null | boolean | number | symbol | string
32
| ReadonlyArray<SerializableParam>
33
| ReadonlySet<SerializableParam>
34
| ReadonlyMap<SerializableParam, SerializableParam>
35
| Readonly<{[key: string]: SerializableParam}>;
36
```
37
38
**Usage Examples:**
39
40
```typescript
41
import { atomFamily, useRecoilState } from 'recoil';
42
43
// Simple atom family with primitive parameter
44
const itemState = atomFamily({
45
key: 'itemState',
46
default: null,
47
});
48
49
// Usage in components
50
function ItemEditor({ itemId }) {
51
const [item, setItem] = useRecoilState(itemState(itemId));
52
53
return (
54
<input
55
value={item || ''}
56
onChange={(e) => setItem(e.target.value)}
57
/>
58
);
59
}
60
61
// Atom family with object parameter
62
const userPreferencesState = atomFamily({
63
key: 'userPreferencesState',
64
default: (userId) => ({
65
theme: 'light',
66
notifications: true,
67
userId,
68
}),
69
});
70
71
// Atom family with async default
72
const userProfileState = atomFamily({
73
key: 'userProfileState',
74
default: async (userId) => {
75
const response = await fetch(`/api/users/${userId}`);
76
return response.json();
77
},
78
});
79
80
// Atom family with parameter-based effects
81
const persistedItemState = atomFamily({
82
key: 'persistedItemState',
83
default: '',
84
effects: (itemId) => [
85
({setSelf, onSet}) => {
86
const key = `item-${itemId}`;
87
const saved = localStorage.getItem(key);
88
if (saved != null) {
89
setSelf(JSON.parse(saved));
90
}
91
92
onSet((newValue) => {
93
localStorage.setItem(key, JSON.stringify(newValue));
94
});
95
},
96
],
97
});
98
```
99
100
### Selector Families
101
102
Creates functions that return memoized selectors based on parameters.
103
104
```typescript { .api }
105
/**
106
* Returns a function which returns a memoized selector for each unique parameter value
107
*/
108
function selectorFamily<T, P extends SerializableParam>(
109
options: ReadWriteSelectorFamilyOptions<T, P>
110
): (param: P) => RecoilState<T>;
111
112
function selectorFamily<T, P extends SerializableParam>(
113
options: ReadOnlySelectorFamilyOptions<T, P>
114
): (param: P) => RecoilValueReadOnly<T>;
115
116
interface ReadOnlySelectorFamilyOptions<T, P extends SerializableParam> {
117
/** Unique string identifying this selector family */
118
key: string;
119
/** Function that computes the selector's value based on parameter */
120
get: (param: P) => (opts: {
121
get: GetRecoilValue;
122
getCallback: GetCallback;
123
}) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
124
/** Cache policy for selectors in this family */
125
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
126
/** Allow direct mutation of selector values */
127
dangerouslyAllowMutability?: boolean;
128
}
129
130
interface ReadWriteSelectorFamilyOptions<T, P extends SerializableParam> {
131
/** Unique string identifying this selector family */
132
key: string;
133
/** Function that computes the selector's value based on parameter */
134
get: (param: P) => (opts: {
135
get: GetRecoilValue;
136
getCallback: GetCallback;
137
}) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
138
/** Function that handles setting the selector's value based on parameter */
139
set: (param: P) => (opts: {
140
set: SetRecoilState;
141
get: GetRecoilValue;
142
reset: ResetRecoilState;
143
}, newValue: T | DefaultValue) => void;
144
/** Cache policy for selectors in this family */
145
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
146
/** Allow direct mutation of selector values */
147
dangerouslyAllowMutability?: boolean;
148
}
149
```
150
151
**Usage Examples:**
152
153
```typescript
154
import { selectorFamily, atomFamily, useRecoilValue } from 'recoil';
155
156
// Read-only selector family
157
const itemWithMetadataState = selectorFamily({
158
key: 'itemWithMetadataState',
159
get: (itemId) => ({get}) => {
160
const item = get(itemState(itemId));
161
const metadata = get(itemMetadataState(itemId));
162
163
return {
164
...item,
165
...metadata,
166
fullId: `item-${itemId}`,
167
};
168
},
169
});
170
171
// Async selector family
172
const userPostsState = selectorFamily({
173
key: 'userPostsState',
174
get: (userId) => async ({get}) => {
175
const user = get(userState(userId));
176
const response = await fetch(`/api/users/${userId}/posts`);
177
return response.json();
178
},
179
});
180
181
// Read-write selector family
182
const itemDisplayNameState = selectorFamily({
183
key: 'itemDisplayNameState',
184
get: (itemId) => ({get}) => {
185
const item = get(itemState(itemId));
186
return item?.name || `Item ${itemId}`;
187
},
188
set: (itemId) => ({set, get}, newValue) => {
189
const currentItem = get(itemState(itemId));
190
set(itemState(itemId), {
191
...currentItem,
192
name: newValue as string,
193
});
194
},
195
});
196
197
// Selector family with complex parameter
198
const filteredListState = selectorFamily({
199
key: 'filteredListState',
200
get: ({listId, filter}) => ({get}) => {
201
const list = get(listState(listId));
202
return list.filter(item =>
203
item.category === filter.category &&
204
item.status === filter.status
205
);
206
},
207
});
208
209
// Usage with object parameter
210
function FilteredList({ listId, category, status }) {
211
const filteredItems = useRecoilValue(filteredListState({
212
listId,
213
filter: { category, status }
214
}));
215
216
return (
217
<ul>
218
{filteredItems.map(item => (
219
<li key={item.id}>{item.name}</li>
220
))}
221
</ul>
222
);
223
}
224
```
225
226
### Parameter-based Effects
227
228
Examples of using effects with families for per-parameter side effects.
229
230
**Usage Examples:**
231
232
```typescript
233
import { atomFamily } from 'recoil';
234
235
// WebSocket connection per chat room
236
const chatRoomState = atomFamily({
237
key: 'chatRoomState',
238
default: { messages: [], connected: false },
239
effects: (roomId) => [
240
({setSelf, onSet}) => {
241
let ws: WebSocket;
242
243
// Connect to room-specific WebSocket
244
const connect = () => {
245
ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);
246
247
ws.onopen = () => {
248
setSelf(current => ({ ...current, connected: true }));
249
};
250
251
ws.onmessage = (event) => {
252
const message = JSON.parse(event.data);
253
setSelf(current => ({
254
...current,
255
messages: [...current.messages, message],
256
}));
257
};
258
259
ws.onclose = () => {
260
setSelf(current => ({ ...current, connected: false }));
261
};
262
};
263
264
// Track when messages are sent
265
onSet((newValue, oldValue) => {
266
if (ws && ws.readyState === WebSocket.OPEN) {
267
const newMessages = newValue.messages;
268
const oldMessages = oldValue.messages || [];
269
270
if (newMessages.length > oldMessages.length) {
271
const lastMessage = newMessages[newMessages.length - 1];
272
if (lastMessage.type === 'outgoing') {
273
ws.send(JSON.stringify(lastMessage));
274
}
275
}
276
}
277
});
278
279
connect();
280
281
// Cleanup
282
return () => {
283
if (ws) {
284
ws.close();
285
}
286
};
287
},
288
],
289
});
290
291
// API cache with per-endpoint invalidation
292
const apiCacheState = atomFamily({
293
key: 'apiCacheState',
294
default: null,
295
effects: (endpoint) => [
296
({setSelf}) => {
297
// Auto-refresh certain endpoints
298
if (endpoint.includes('/live-data/')) {
299
const interval = setInterval(async () => {
300
try {
301
const response = await fetch(endpoint);
302
const data = await response.json();
303
setSelf(data);
304
} catch (error) {
305
console.error(`Failed to refresh ${endpoint}:`, error);
306
}
307
}, 5000);
308
309
return () => clearInterval(interval);
310
}
311
},
312
],
313
});
314
```
315
316
### Best Practices
317
318
**Parameter Design:**
319
- Use serializable parameters only (primitives, arrays, objects, Maps, Sets)
320
- Keep parameters immutable to ensure proper memoization
321
- Use object parameters for complex combinations of values
322
- Consider parameter normalization for consistent cache keys
323
324
**Performance Considerations:**
325
- Family instances are memoized by parameter reference/value
326
- Avoid creating new parameter objects on every render
327
- Use useMemo for complex parameter objects
328
- Consider cache policies for selector families with expensive computations
329
330
**Usage Examples:**
331
332
```typescript
333
import React, { useMemo } from 'react';
334
import { selectorFamily, useRecoilValue } from 'recoil';
335
336
// Good: Stable parameter object
337
function UserDashboard({ userId, filters }) {
338
const filterParams = useMemo(() => ({
339
userId,
340
...filters,
341
}), [userId, filters]);
342
343
const dashboardData = useRecoilValue(userDashboardState(filterParams));
344
345
return <div>{/* render dashboard */}</div>;
346
}
347
348
// Bad: New object on every render
349
function UserDashboardBad({ userId, filters }) {
350
// This creates a new parameter object on every render!
351
const dashboardData = useRecoilValue(userDashboardState({
352
userId,
353
...filters,
354
}));
355
356
return <div>{/* render dashboard */}</div>;
357
}
358
```