0
# Loadable System
1
2
System for handling async state with loading, error, and success states. Loadables provide a unified interface for working with synchronous values, promises, and error states without using React suspense boundaries.
3
4
## Capabilities
5
6
### Loadable Types
7
8
Core loadable interface and its variants for different states.
9
10
```typescript { .api }
11
/**
12
* Discriminated union representing the state of async operations
13
*/
14
type Loadable<T> = ValueLoadable<T> | LoadingLoadable<T> | ErrorLoadable<T>;
15
16
interface BaseLoadable<T> {
17
/** Get the value, throwing if not available */
18
getValue: () => T;
19
/** Convert to a Promise */
20
toPromise: () => Promise<T>;
21
/** Get value or throw error/promise */
22
valueOrThrow: () => T;
23
/** Get error or throw if not error state */
24
errorOrThrow: () => any;
25
/** Get promise or throw if not loading */
26
promiseOrThrow: () => Promise<T>;
27
/** Check equality with another loadable */
28
is: (other: Loadable<any>) => boolean;
29
/** Transform the loadable value */
30
map: <S>(map: (from: T) => Loadable<S> | Promise<S> | S) => Loadable<S>;
31
}
32
33
interface ValueLoadable<T> extends BaseLoadable<T> {
34
state: 'hasValue';
35
contents: T;
36
/** Get value if available, undefined otherwise */
37
valueMaybe: () => T;
38
/** Get error if available, undefined otherwise */
39
errorMaybe: () => undefined;
40
/** Get promise if available, undefined otherwise */
41
promiseMaybe: () => undefined;
42
}
43
44
interface LoadingLoadable<T> extends BaseLoadable<T> {
45
state: 'loading';
46
contents: Promise<T>;
47
valueMaybe: () => undefined;
48
errorMaybe: () => undefined;
49
promiseMaybe: () => Promise<T>;
50
}
51
52
interface ErrorLoadable<T> extends BaseLoadable<T> {
53
state: 'hasError';
54
contents: any;
55
valueMaybe: () => undefined;
56
errorMaybe: () => any;
57
promiseMaybe: () => undefined;
58
}
59
```
60
61
**Usage Examples:**
62
63
```typescript
64
import React from 'react';
65
import { useRecoilValueLoadable } from 'recoil';
66
67
// Component handling all loadable states
68
function AsyncDataDisplay({ dataState }) {
69
const dataLoadable = useRecoilValueLoadable(dataState);
70
71
switch (dataLoadable.state) {
72
case 'hasValue':
73
return <div>Data: {JSON.stringify(dataLoadable.contents)}</div>;
74
75
case 'loading':
76
return <div>Loading...</div>;
77
78
case 'hasError':
79
return <div>Error: {dataLoadable.contents.message}</div>;
80
}
81
}
82
83
// Using loadable methods
84
function LoadableMethodsExample({ dataState }) {
85
const dataLoadable = useRecoilValueLoadable(dataState);
86
87
// Safe value access
88
const value = dataLoadable.valueMaybe();
89
const error = dataLoadable.errorMaybe();
90
const promise = dataLoadable.promiseMaybe();
91
92
return (
93
<div>
94
{value && <div>Value: {JSON.stringify(value)}</div>}
95
{error && <div>Error: {error.message}</div>}
96
{promise && <div>Loading...</div>}
97
</div>
98
);
99
}
100
101
// Transform loadable values
102
function TransformedLoadable({ userState }) {
103
const userLoadable = useRecoilValueLoadable(userState);
104
105
// Transform the loadable to get display name
106
const displayNameLoadable = userLoadable.map(user =>
107
user.displayName || user.email || 'Anonymous'
108
);
109
110
if (displayNameLoadable.state === 'hasValue') {
111
return <div>Welcome, {displayNameLoadable.contents}!</div>;
112
}
113
114
return <div>Loading user...</div>;
115
}
116
```
117
118
### RecoilLoadable Namespace
119
120
Factory functions and utilities for creating and working with loadables.
121
122
```typescript { .api }
123
namespace RecoilLoadable {
124
/**
125
* Factory to make a Loadable object. If a Promise is provided the Loadable will
126
* be in a 'loading' state until the Promise is either resolved or rejected.
127
*/
128
function of<T>(x: T | Promise<T> | Loadable<T>): Loadable<T>;
129
130
/**
131
* Factory to make a Loadable object in an error state
132
*/
133
function error(x: any): ErrorLoadable<any>;
134
135
/**
136
* Factory to make a loading Loadable which never resolves
137
*/
138
function loading(): LoadingLoadable<any>;
139
140
/**
141
* Factory to make a Loadable which is resolved when all of the Loadables provided
142
* to it are resolved or any one has an error. The value is an array of the values
143
* of all of the provided Loadables. This is comparable to Promise.all() for Loadables.
144
*/
145
function all<Inputs extends any[] | [Loadable<any>]>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
146
function all<Inputs extends {[key: string]: any}>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
147
148
/**
149
* Returns true if the provided parameter is a Loadable type
150
*/
151
function isLoadable(x: any): x is Loadable<any>;
152
}
153
154
type UnwrapLoadables<T extends any[] | { [key: string]: any }> = {
155
[P in keyof T]: UnwrapLoadable<T[P]>;
156
};
157
158
type UnwrapLoadable<T> = T extends Loadable<infer R> ? R : T extends Promise<infer P> ? P : T;
159
```
160
161
**Usage Examples:**
162
163
```typescript
164
import { RecoilLoadable, selector } from 'recoil';
165
166
// Create loadables from various inputs
167
const exampleSelector = selector({
168
key: 'exampleSelector',
169
get: () => {
170
// From value
171
const valueLoadable = RecoilLoadable.of('hello');
172
173
// From promise
174
const promiseLoadable = RecoilLoadable.of(
175
fetch('/api/data').then(r => r.json())
176
);
177
178
// Error loadable
179
const errorLoadable = RecoilLoadable.error(new Error('Something went wrong'));
180
181
// Loading loadable that never resolves
182
const loadingLoadable = RecoilLoadable.loading();
183
184
return { valueLoadable, promiseLoadable, errorLoadable, loadingLoadable };
185
},
186
});
187
188
// Combine multiple loadables
189
const combinedDataSelector = selector({
190
key: 'combinedDataSelector',
191
get: async ({get}) => {
192
const userLoadable = get(noWait(userState));
193
const settingsLoadable = get(noWait(settingsState));
194
const preferencesLoadable = get(noWait(preferencesState));
195
196
// Wait for all to resolve
197
const combinedLoadable = RecoilLoadable.all([
198
userLoadable,
199
settingsLoadable,
200
preferencesLoadable,
201
]);
202
203
if (combinedLoadable.state === 'hasValue') {
204
const [user, settings, preferences] = combinedLoadable.contents;
205
return { user, settings, preferences };
206
}
207
208
// Propagate loading or error state
209
return combinedLoadable.contents;
210
},
211
});
212
213
// Combine object of loadables
214
const dashboardDataSelector = selector({
215
key: 'dashboardDataSelector',
216
get: async ({get}) => {
217
const loadables = {
218
user: get(noWait(userState)),
219
posts: get(noWait(postsState)),
220
notifications: get(noWait(notificationsState)),
221
};
222
223
const combinedLoadable = RecoilLoadable.all(loadables);
224
225
if (combinedLoadable.state === 'hasValue') {
226
return {
227
...combinedLoadable.contents,
228
summary: `${combinedLoadable.contents.posts.length} posts, ${combinedLoadable.contents.notifications.length} notifications`,
229
};
230
}
231
232
throw combinedLoadable.contents;
233
},
234
});
235
236
// Type checking
237
function processUnknownValue(value: unknown) {
238
if (RecoilLoadable.isLoadable(value)) {
239
switch (value.state) {
240
case 'hasValue':
241
console.log('Loadable value:', value.contents);
242
break;
243
case 'loading':
244
console.log('Loadable is loading');
245
break;
246
case 'hasError':
247
console.log('Loadable error:', value.contents);
248
break;
249
}
250
} else {
251
console.log('Not a loadable:', value);
252
}
253
}
254
```
255
256
### Loadable Patterns
257
258
Common patterns for working with loadables in complex scenarios.
259
260
**Usage Examples:**
261
262
```typescript
263
import React from 'react';
264
import { RecoilLoadable, selector, useRecoilValue } from 'recoil';
265
266
// Fallback chain with loadables
267
const dataWithFallbackSelector = selector({
268
key: 'dataWithFallbackSelector',
269
get: ({get}) => {
270
const primaryLoadable = get(noWait(primaryDataState));
271
const secondaryLoadable = get(noWait(secondaryDataState));
272
const cacheLoadable = get(noWait(cacheDataState));
273
274
// Try primary first
275
if (primaryLoadable.state === 'hasValue') {
276
return RecoilLoadable.of({
277
data: primaryLoadable.contents,
278
source: 'primary',
279
});
280
}
281
282
// Try secondary
283
if (secondaryLoadable.state === 'hasValue') {
284
return RecoilLoadable.of({
285
data: secondaryLoadable.contents,
286
source: 'secondary',
287
});
288
}
289
290
// Use cache as last resort
291
if (cacheLoadable.state === 'hasValue') {
292
return RecoilLoadable.of({
293
data: cacheLoadable.contents,
294
source: 'cache',
295
stale: true,
296
});
297
}
298
299
// All are loading or have errors
300
if (primaryLoadable.state === 'loading' ||
301
secondaryLoadable.state === 'loading') {
302
return RecoilLoadable.loading();
303
}
304
305
// Return the primary error as it's most important
306
return RecoilLoadable.error(primaryLoadable.contents);
307
},
308
});
309
310
// Partial data accumulator
311
const partialDataSelector = selector({
312
key: 'partialDataSelector',
313
get: ({get}) => {
314
const loadables = {
315
essential: get(noWait(essentialDataState)),
316
important: get(noWait(importantDataState)),
317
optional: get(noWait(optionalDataState)),
318
};
319
320
const result = {
321
essential: null,
322
important: null,
323
optional: null,
324
status: 'partial',
325
};
326
327
// Must have essential data
328
if (loadables.essential.state !== 'hasValue') {
329
if (loadables.essential.state === 'hasError') {
330
return RecoilLoadable.error(loadables.essential.contents);
331
}
332
return RecoilLoadable.loading();
333
}
334
335
result.essential = loadables.essential.contents;
336
337
// Include other data if available
338
if (loadables.important.state === 'hasValue') {
339
result.important = loadables.important.contents;
340
}
341
342
if (loadables.optional.state === 'hasValue') {
343
result.optional = loadables.optional.contents;
344
}
345
346
// Mark as complete if we have everything
347
if (result.important && result.optional) {
348
result.status = 'complete';
349
}
350
351
return RecoilLoadable.of(result);
352
},
353
});
354
355
// Loadable transformation chain
356
const processedDataSelector = selector({
357
key: 'processedDataSelector',
358
get: ({get}) => {
359
const dataLoadable = get(noWait(rawDataState));
360
361
// Chain transformations on the loadable
362
return dataLoadable
363
.map(data => data.filter(item => item.active))
364
.map(data => data.map(item => ({
365
...item,
366
displayName: item.name.toUpperCase(),
367
})))
368
.map(data => data.sort((a, b) => a.priority - b.priority));
369
},
370
});
371
372
// Component with sophisticated loadable handling
373
function SmartDataComponent() {
374
const dataLoadable = useRecoilValue(noWait(partialDataSelector));
375
376
if (dataLoadable.state === 'loading') {
377
return <div>Loading essential data...</div>;
378
}
379
380
if (dataLoadable.state === 'hasError') {
381
return <div>Failed to load: {dataLoadable.contents.message}</div>;
382
}
383
384
const data = dataLoadable.contents;
385
386
return (
387
<div>
388
<div>Essential: {JSON.stringify(data.essential)}</div>
389
390
{data.important ? (
391
<div>Important: {JSON.stringify(data.important)}</div>
392
) : (
393
<div>Loading important data...</div>
394
)}
395
396
{data.optional ? (
397
<div>Optional: {JSON.stringify(data.optional)}</div>
398
) : (
399
<div>Optional data unavailable</div>
400
)}
401
402
<div>Status: {data.status}</div>
403
</div>
404
);
405
}
406
```
407
408
## Error Handling Patterns
409
410
**Graceful Degradation:**
411
- Use loadables to provide partial functionality when some data fails
412
- Implement fallback chains for resilient data loading
413
- Show appropriate loading states while preserving usability
414
415
**Error Recovery:**
416
- Transform error loadables into default values where appropriate
417
- Implement retry mechanisms using loadable state information
418
- Provide user-friendly error messages with recovery options