0
# Concurrency Helpers
1
2
Utilities for coordinating multiple async operations and handling loading states. These helpers provide fine-grained control over how async selectors behave and allow for sophisticated async coordination patterns.
3
4
## Capabilities
5
6
### NoWait Pattern
7
8
Wraps a Recoil value to avoid suspense and error boundaries, returning a Loadable instead.
9
10
```typescript { .api }
11
/**
12
* Returns a selector that has the value of the provided atom or selector as a Loadable.
13
* This means you can use noWait() to avoid entering an error or suspense state in
14
* order to manually handle those cases.
15
*/
16
function noWait<T>(state: RecoilValue<T>): RecoilValueReadOnly<Loadable<T>>;
17
```
18
19
**Usage Examples:**
20
21
```typescript
22
import React from 'react';
23
import { noWait, useRecoilValue } from 'recoil';
24
25
// Component that handles async state manually
26
function UserProfile({ userId }) {
27
const userProfileLoadable = useRecoilValue(noWait(userProfileState(userId)));
28
29
switch (userProfileLoadable.state) {
30
case 'hasValue':
31
return <div>Welcome, {userProfileLoadable.contents.name}!</div>;
32
case 'loading':
33
return <div>Loading profile...</div>;
34
case 'hasError':
35
throw userProfileLoadable.contents; // Re-throw if needed
36
return <div>Error: {userProfileLoadable.contents.message}</div>;
37
}
38
}
39
40
// Selector that handles errors gracefully
41
const safeUserDataState = selector({
42
key: 'safeUserDataState',
43
get: ({get}) => {
44
const userLoadable = get(noWait(userState));
45
const preferencesLoadable = get(noWait(userPreferencesState));
46
47
return {
48
user: userLoadable.state === 'hasValue' ? userLoadable.contents : null,
49
preferences: preferencesLoadable.state === 'hasValue' ? preferencesLoadable.contents : {},
50
errors: {
51
user: userLoadable.state === 'hasError' ? userLoadable.contents : null,
52
preferences: preferencesLoadable.state === 'hasError' ? preferencesLoadable.contents : null,
53
}
54
};
55
},
56
});
57
```
58
59
### Wait for All
60
61
Waits for all provided Recoil values to resolve, similar to Promise.all().
62
63
```typescript { .api }
64
/**
65
* Waits for all values to resolve, returns unwrapped values
66
*/
67
function waitForAll<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
68
param: RecoilValues
69
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
70
71
function waitForAll<RecoilValues extends { [key: string]: RecoilValue<any> }>(
72
param: RecoilValues
73
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
74
75
type UnwrapRecoilValues<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
76
[P in keyof T]: UnwrapRecoilValue<T[P]>;
77
};
78
```
79
80
**Usage Examples:**
81
82
```typescript
83
import { waitForAll, selector, useRecoilValue } from 'recoil';
84
85
// Wait for multiple async selectors (array form)
86
const dashboardDataState = selector({
87
key: 'dashboardDataState',
88
get: ({get}) => {
89
const [user, posts, notifications] = get(waitForAll([
90
userState,
91
userPostsState,
92
userNotificationsState,
93
]));
94
95
return {
96
user,
97
posts,
98
notifications,
99
summary: `${posts.length} posts, ${notifications.length} notifications`,
100
};
101
},
102
});
103
104
// Wait for multiple async selectors (object form)
105
const userDashboardState = selector({
106
key: 'userDashboardState',
107
get: ({get}) => {
108
const data = get(waitForAll({
109
profile: userProfileState,
110
settings: userSettingsState,
111
activity: userActivityState,
112
}));
113
114
return {
115
...data,
116
lastLogin: data.activity.lastLogin,
117
displayName: data.profile.displayName || data.profile.email,
118
};
119
},
120
});
121
122
// Component using waitForAll
123
function Dashboard() {
124
const dashboardData = useRecoilValue(dashboardDataState);
125
126
return (
127
<div>
128
<h1>Welcome, {dashboardData.user.name}</h1>
129
<p>{dashboardData.summary}</p>
130
<PostsList posts={dashboardData.posts} />
131
<NotificationsList notifications={dashboardData.notifications} />
132
</div>
133
);
134
}
135
```
136
137
### Wait for Any
138
139
Waits for any of the provided values to resolve, returns all as Loadables.
140
141
```typescript { .api }
142
/**
143
* Waits for any value to resolve, returns all as Loadables
144
*/
145
function waitForAny<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
146
param: RecoilValues
147
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
148
149
function waitForAny<RecoilValues extends { [key: string]: RecoilValue<any> }>(
150
param: RecoilValues
151
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
152
153
type UnwrapRecoilValueLoadables<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
154
[P in keyof T]: Loadable<UnwrapRecoilValue<T[P]>>;
155
};
156
```
157
158
**Usage Examples:**
159
160
```typescript
161
import { waitForAny, selector, useRecoilValue } from 'recoil';
162
163
// Show data as soon as any source is available
164
const quickDataState = selector({
165
key: 'quickDataState',
166
get: ({get}) => {
167
const [cacheLoadable, apiLoadable] = get(waitForAny([
168
cachedDataState,
169
freshApiDataState,
170
]));
171
172
// Use cached data if available, otherwise wait for API
173
if (cacheLoadable.state === 'hasValue') {
174
return {
175
data: cacheLoadable.contents,
176
source: 'cache',
177
fresh: false,
178
};
179
}
180
181
if (apiLoadable.state === 'hasValue') {
182
return {
183
data: apiLoadable.contents,
184
source: 'api',
185
fresh: true,
186
};
187
}
188
189
// Still loading
190
throw new Promise(() => {}); // Suspend until something resolves
191
},
192
});
193
194
// Race between multiple data sources
195
const raceDataState = selector({
196
key: 'raceDataState',
197
get: ({get}) => {
198
const sources = get(waitForAny({
199
primary: primaryApiState,
200
fallback: fallbackApiState,
201
cache: cacheState,
202
}));
203
204
// Return first available source
205
for (const [sourceName, loadable] of Object.entries(sources)) {
206
if (loadable.state === 'hasValue') {
207
return {
208
data: loadable.contents,
209
source: sourceName,
210
};
211
}
212
}
213
214
// Check for errors
215
const errors = Object.entries(sources)
216
.filter(([_, loadable]) => loadable.state === 'hasError')
217
.map(([name, loadable]) => ({ source: name, error: loadable.contents }));
218
219
if (errors.length === Object.keys(sources).length) {
220
throw new Error(`All sources failed: ${errors.map(e => e.source).join(', ')}`);
221
}
222
223
// Still loading
224
throw new Promise(() => {});
225
},
226
});
227
```
228
229
### Wait for None
230
231
Returns all values as Loadables immediately without waiting for any to resolve.
232
233
```typescript { .api }
234
/**
235
* Returns loadables immediately without waiting for any to resolve
236
*/
237
function waitForNone<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
238
param: RecoilValues
239
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
240
241
function waitForNone<RecoilValues extends { [key: string]: RecoilValue<any> }>(
242
param: RecoilValues
243
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
244
```
245
246
**Usage Examples:**
247
248
```typescript
249
import { waitForNone, selector, useRecoilValue } from 'recoil';
250
251
// Check loading states of multiple async operations
252
const loadingStatusState = selector({
253
key: 'loadingStatusState',
254
get: ({get}) => {
255
const [userLoadable, postsLoadable, notificationsLoadable] = get(waitForNone([
256
userState,
257
postsState,
258
notificationsState,
259
]));
260
261
return {
262
user: userLoadable.state,
263
posts: postsLoadable.state,
264
notifications: notificationsLoadable.state,
265
allLoaded: [userLoadable, postsLoadable, notificationsLoadable]
266
.every(l => l.state === 'hasValue'),
267
anyErrors: [userLoadable, postsLoadable, notificationsLoadable]
268
.some(l => l.state === 'hasError'),
269
};
270
},
271
});
272
273
// Progressive loading component
274
function ProgressiveLoader() {
275
const status = useRecoilValue(loadingStatusState);
276
277
return (
278
<div>
279
<div>User: {status.user}</div>
280
<div>Posts: {status.posts}</div>
281
<div>Notifications: {status.notifications}</div>
282
{status.allLoaded && <div>✅ All data loaded!</div>}
283
{status.anyErrors && <div>❌ Some data failed to load</div>}
284
</div>
285
);
286
}
287
288
// Incremental data display
289
const incrementalDataState = selector({
290
key: 'incrementalDataState',
291
get: ({get}) => {
292
const dataLoadables = get(waitForNone({
293
essential: essentialDataState,
294
secondary: secondaryDataState,
295
optional: optionalDataState,
296
}));
297
298
const result = {
299
essential: null,
300
secondary: null,
301
optional: null,
302
loadingCount: 0,
303
errorCount: 0,
304
};
305
306
Object.entries(dataLoadables).forEach(([key, loadable]) => {
307
switch (loadable.state) {
308
case 'hasValue':
309
result[key] = loadable.contents;
310
break;
311
case 'loading':
312
result.loadingCount++;
313
break;
314
case 'hasError':
315
result.errorCount++;
316
break;
317
}
318
});
319
320
return result;
321
},
322
});
323
```
324
325
### Wait for All Settled
326
327
Waits for all values to settle (resolve or reject), returning all as Loadables.
328
329
```typescript { .api }
330
/**
331
* Waits for all values to settle (resolve or reject), returns all as Loadables
332
*/
333
function waitForAllSettled<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
334
param: RecoilValues
335
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
336
337
function waitForAllSettled<RecoilValues extends { [key: string]: RecoilValue<any> }>(
338
param: RecoilValues
339
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
340
```
341
342
**Usage Examples:**
343
344
```typescript
345
import { waitForAllSettled, selector, useRecoilValue } from 'recoil';
346
347
// Aggregate results even when some fail
348
const aggregateDataState = selector({
349
key: 'aggregateDataState',
350
get: ({get}) => {
351
const results = get(waitForAllSettled([
352
criticalDataState,
353
optionalDataState,
354
supplementaryDataState,
355
]));
356
357
const [criticalLoadable, optionalLoadable, supplementaryLoadable] = results;
358
359
// Must have critical data
360
if (criticalLoadable.state !== 'hasValue') {
361
throw criticalLoadable.state === 'hasError'
362
? criticalLoadable.contents
363
: new Error('Critical data still loading');
364
}
365
366
return {
367
critical: criticalLoadable.contents,
368
optional: optionalLoadable.state === 'hasValue' ? optionalLoadable.contents : null,
369
supplementary: supplementaryLoadable.state === 'hasValue' ? supplementaryLoadable.contents : null,
370
errors: {
371
optional: optionalLoadable.state === 'hasError' ? optionalLoadable.contents : null,
372
supplementary: supplementaryLoadable.state === 'hasError' ? supplementaryLoadable.contents : null,
373
},
374
};
375
},
376
});
377
378
// Report generation that includes partial results
379
const reportState = selector({
380
key: 'reportState',
381
get: ({get}) => {
382
const sections = get(waitForAllSettled({
383
summary: summaryDataState,
384
details: detailsDataState,
385
charts: chartsDataState,
386
appendix: appendixDataState,
387
}));
388
389
const report = {
390
timestamp: new Date().toISOString(),
391
sections: {},
392
errors: [],
393
warnings: [],
394
};
395
396
Object.entries(sections).forEach(([sectionName, loadable]) => {
397
switch (loadable.state) {
398
case 'hasValue':
399
report.sections[sectionName] = loadable.contents;
400
break;
401
case 'hasError':
402
report.errors.push({
403
section: sectionName,
404
error: loadable.contents.message,
405
});
406
break;
407
case 'loading':
408
report.warnings.push(`Section ${sectionName} is still loading`);
409
break;
410
}
411
});
412
413
return report;
414
},
415
});
416
417
// Component that shows partial results
418
function ReportViewer() {
419
const report = useRecoilValue(reportState);
420
421
return (
422
<div>
423
<h1>Report ({report.timestamp})</h1>
424
425
{Object.entries(report.sections).map(([name, data]) => (
426
<div key={name}>
427
<h2>{name}</h2>
428
<pre>{JSON.stringify(data, null, 2)}</pre>
429
</div>
430
))}
431
432
{report.errors.length > 0 && (
433
<div>
434
<h2>Errors</h2>
435
{report.errors.map((error, i) => (
436
<div key={i}>
437
{error.section}: {error.error}
438
</div>
439
))}
440
</div>
441
)}
442
443
{report.warnings.length > 0 && (
444
<div>
445
<h2>Warnings</h2>
446
{report.warnings.map((warning, i) => (
447
<div key={i}>{warning}</div>
448
))}
449
</div>
450
)}
451
</div>
452
);
453
}
454
```
455
456
## Coordination Patterns
457
458
**Common Use Cases:**
459
460
1. **Progressive Loading**: Use `waitForNone` to show data as it becomes available
461
2. **Fallback Chains**: Use `waitForAny` to implement fallback data sources
462
3. **Data Aggregation**: Use `waitForAll` when all data is required
463
4. **Resilient Loading**: Use `waitForAllSettled` when some failures are acceptable
464
5. **Manual Error Handling**: Use `noWait` to handle errors without suspense boundaries