CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-recoil

Recoil is an experimental state management framework for React applications that provides atoms and selectors for fine-grained reactivity.

Pending
Overview
Eval results
Files

concurrency-helpers.mddocs/

Concurrency Helpers

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.

Capabilities

NoWait Pattern

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,
      }
    };
  },
});

Wait for All

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>
  );
}

Wait for Any

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(() => {});
  },
});

Wait for None

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;
  },
});

Wait for All Settled

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>
  );
}

Coordination Patterns

Common Use Cases:

  1. Progressive Loading: Use waitForNone to show data as it becomes available
  2. Fallback Chains: Use waitForAny to implement fallback data sources
  3. Data Aggregation: Use waitForAll when all data is required
  4. Resilient Loading: Use waitForAllSettled when some failures are acceptable
  5. Manual Error Handling: Use noWait to handle errors without suspense boundaries

Install with Tessl CLI

npx tessl i tessl/npm-recoil

docs

advanced-hooks.md

concurrency-helpers.md

core-hooks.md

family-patterns.md

index.md

loadable-system.md

memory-management.md

root-provider.md

state-definition.md

tile.json