Recoil is an experimental state management framework for React applications that provides atoms and selectors for fine-grained reactivity.
—
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.
Wraps a Recoil value to avoid suspense and error boundaries, returning a Loadable instead.
/**
* Returns a selector that has the value of the provided atom or selector as a Loadable.
* This means you can use noWait() to avoid entering an error or suspense state in
* order to manually handle those cases.
*/
function noWait<T>(state: RecoilValue<T>): RecoilValueReadOnly<Loadable<T>>;Usage Examples:
import React from 'react';
import { noWait, useRecoilValue } from 'recoil';
// Component that handles async state manually
function UserProfile({ userId }) {
const userProfileLoadable = useRecoilValue(noWait(userProfileState(userId)));
switch (userProfileLoadable.state) {
case 'hasValue':
return <div>Welcome, {userProfileLoadable.contents.name}!</div>;
case 'loading':
return <div>Loading profile...</div>;
case 'hasError':
throw userProfileLoadable.contents; // Re-throw if needed
return <div>Error: {userProfileLoadable.contents.message}</div>;
}
}
// Selector that handles errors gracefully
const safeUserDataState = selector({
key: 'safeUserDataState',
get: ({get}) => {
const userLoadable = get(noWait(userState));
const preferencesLoadable = get(noWait(userPreferencesState));
return {
user: userLoadable.state === 'hasValue' ? userLoadable.contents : null,
preferences: preferencesLoadable.state === 'hasValue' ? preferencesLoadable.contents : {},
errors: {
user: userLoadable.state === 'hasError' ? userLoadable.contents : null,
preferences: preferencesLoadable.state === 'hasError' ? preferencesLoadable.contents : null,
}
};
},
});Waits for all provided Recoil values to resolve, similar to Promise.all().
/**
* Waits for all values to resolve, returns unwrapped values
*/
function waitForAll<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
function waitForAll<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
type UnwrapRecoilValues<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
[P in keyof T]: UnwrapRecoilValue<T[P]>;
};Usage Examples:
import { waitForAll, selector, useRecoilValue } from 'recoil';
// Wait for multiple async selectors (array form)
const dashboardDataState = selector({
key: 'dashboardDataState',
get: ({get}) => {
const [user, posts, notifications] = get(waitForAll([
userState,
userPostsState,
userNotificationsState,
]));
return {
user,
posts,
notifications,
summary: `${posts.length} posts, ${notifications.length} notifications`,
};
},
});
// Wait for multiple async selectors (object form)
const userDashboardState = selector({
key: 'userDashboardState',
get: ({get}) => {
const data = get(waitForAll({
profile: userProfileState,
settings: userSettingsState,
activity: userActivityState,
}));
return {
...data,
lastLogin: data.activity.lastLogin,
displayName: data.profile.displayName || data.profile.email,
};
},
});
// Component using waitForAll
function Dashboard() {
const dashboardData = useRecoilValue(dashboardDataState);
return (
<div>
<h1>Welcome, {dashboardData.user.name}</h1>
<p>{dashboardData.summary}</p>
<PostsList posts={dashboardData.posts} />
<NotificationsList notifications={dashboardData.notifications} />
</div>
);
}Waits for any of the provided values to resolve, returns all as Loadables.
/**
* Waits for any value to resolve, returns all as Loadables
*/
function waitForAny<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
function waitForAny<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
type UnwrapRecoilValueLoadables<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
[P in keyof T]: Loadable<UnwrapRecoilValue<T[P]>>;
};Usage Examples:
import { waitForAny, selector, useRecoilValue } from 'recoil';
// Show data as soon as any source is available
const quickDataState = selector({
key: 'quickDataState',
get: ({get}) => {
const [cacheLoadable, apiLoadable] = get(waitForAny([
cachedDataState,
freshApiDataState,
]));
// Use cached data if available, otherwise wait for API
if (cacheLoadable.state === 'hasValue') {
return {
data: cacheLoadable.contents,
source: 'cache',
fresh: false,
};
}
if (apiLoadable.state === 'hasValue') {
return {
data: apiLoadable.contents,
source: 'api',
fresh: true,
};
}
// Still loading
throw new Promise(() => {}); // Suspend until something resolves
},
});
// Race between multiple data sources
const raceDataState = selector({
key: 'raceDataState',
get: ({get}) => {
const sources = get(waitForAny({
primary: primaryApiState,
fallback: fallbackApiState,
cache: cacheState,
}));
// Return first available source
for (const [sourceName, loadable] of Object.entries(sources)) {
if (loadable.state === 'hasValue') {
return {
data: loadable.contents,
source: sourceName,
};
}
}
// Check for errors
const errors = Object.entries(sources)
.filter(([_, loadable]) => loadable.state === 'hasError')
.map(([name, loadable]) => ({ source: name, error: loadable.contents }));
if (errors.length === Object.keys(sources).length) {
throw new Error(`All sources failed: ${errors.map(e => e.source).join(', ')}`);
}
// Still loading
throw new Promise(() => {});
},
});Returns all values as Loadables immediately without waiting for any to resolve.
/**
* Returns loadables immediately without waiting for any to resolve
*/
function waitForNone<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
function waitForNone<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;Usage Examples:
import { waitForNone, selector, useRecoilValue } from 'recoil';
// Check loading states of multiple async operations
const loadingStatusState = selector({
key: 'loadingStatusState',
get: ({get}) => {
const [userLoadable, postsLoadable, notificationsLoadable] = get(waitForNone([
userState,
postsState,
notificationsState,
]));
return {
user: userLoadable.state,
posts: postsLoadable.state,
notifications: notificationsLoadable.state,
allLoaded: [userLoadable, postsLoadable, notificationsLoadable]
.every(l => l.state === 'hasValue'),
anyErrors: [userLoadable, postsLoadable, notificationsLoadable]
.some(l => l.state === 'hasError'),
};
},
});
// Progressive loading component
function ProgressiveLoader() {
const status = useRecoilValue(loadingStatusState);
return (
<div>
<div>User: {status.user}</div>
<div>Posts: {status.posts}</div>
<div>Notifications: {status.notifications}</div>
{status.allLoaded && <div>✅ All data loaded!</div>}
{status.anyErrors && <div>❌ Some data failed to load</div>}
</div>
);
}
// Incremental data display
const incrementalDataState = selector({
key: 'incrementalDataState',
get: ({get}) => {
const dataLoadables = get(waitForNone({
essential: essentialDataState,
secondary: secondaryDataState,
optional: optionalDataState,
}));
const result = {
essential: null,
secondary: null,
optional: null,
loadingCount: 0,
errorCount: 0,
};
Object.entries(dataLoadables).forEach(([key, loadable]) => {
switch (loadable.state) {
case 'hasValue':
result[key] = loadable.contents;
break;
case 'loading':
result.loadingCount++;
break;
case 'hasError':
result.errorCount++;
break;
}
});
return result;
},
});Waits for all values to settle (resolve or reject), returning all as Loadables.
/**
* Waits for all values to settle (resolve or reject), returns all as Loadables
*/
function waitForAllSettled<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
function waitForAllSettled<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;Usage Examples:
import { waitForAllSettled, selector, useRecoilValue } from 'recoil';
// Aggregate results even when some fail
const aggregateDataState = selector({
key: 'aggregateDataState',
get: ({get}) => {
const results = get(waitForAllSettled([
criticalDataState,
optionalDataState,
supplementaryDataState,
]));
const [criticalLoadable, optionalLoadable, supplementaryLoadable] = results;
// Must have critical data
if (criticalLoadable.state !== 'hasValue') {
throw criticalLoadable.state === 'hasError'
? criticalLoadable.contents
: new Error('Critical data still loading');
}
return {
critical: criticalLoadable.contents,
optional: optionalLoadable.state === 'hasValue' ? optionalLoadable.contents : null,
supplementary: supplementaryLoadable.state === 'hasValue' ? supplementaryLoadable.contents : null,
errors: {
optional: optionalLoadable.state === 'hasError' ? optionalLoadable.contents : null,
supplementary: supplementaryLoadable.state === 'hasError' ? supplementaryLoadable.contents : null,
},
};
},
});
// Report generation that includes partial results
const reportState = selector({
key: 'reportState',
get: ({get}) => {
const sections = get(waitForAllSettled({
summary: summaryDataState,
details: detailsDataState,
charts: chartsDataState,
appendix: appendixDataState,
}));
const report = {
timestamp: new Date().toISOString(),
sections: {},
errors: [],
warnings: [],
};
Object.entries(sections).forEach(([sectionName, loadable]) => {
switch (loadable.state) {
case 'hasValue':
report.sections[sectionName] = loadable.contents;
break;
case 'hasError':
report.errors.push({
section: sectionName,
error: loadable.contents.message,
});
break;
case 'loading':
report.warnings.push(`Section ${sectionName} is still loading`);
break;
}
});
return report;
},
});
// Component that shows partial results
function ReportViewer() {
const report = useRecoilValue(reportState);
return (
<div>
<h1>Report ({report.timestamp})</h1>
{Object.entries(report.sections).map(([name, data]) => (
<div key={name}>
<h2>{name}</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
))}
{report.errors.length > 0 && (
<div>
<h2>Errors</h2>
{report.errors.map((error, i) => (
<div key={i}>
{error.section}: {error.error}
</div>
))}
</div>
)}
{report.warnings.length > 0 && (
<div>
<h2>Warnings</h2>
{report.warnings.map((warning, i) => (
<div key={i}>{warning}</div>
))}
</div>
)}
</div>
);
}Common Use Cases:
waitForNone to show data as it becomes availablewaitForAny to implement fallback data sourceswaitForAll when all data is requiredwaitForAllSettled when some failures are acceptablenoWait to handle errors without suspense boundariesInstall with Tessl CLI
npx tessl i tessl/npm-recoil