or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-hooks.mdconcurrency-helpers.mdcore-hooks.mdfamily-patterns.mdindex.mdloadable-system.mdmemory-management.mdroot-provider.mdstate-definition.md

family-patterns.mddocs/

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

```