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

loadable-system.mddocs/

Loadable System

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.

Capabilities

Loadable Types

Core loadable interface and its variants for different states.

/**
 * Discriminated union representing the state of async operations
 */
type Loadable<T> = ValueLoadable<T> | LoadingLoadable<T> | ErrorLoadable<T>;

interface BaseLoadable<T> {
  /** Get the value, throwing if not available */
  getValue: () => T;
  /** Convert to a Promise */
  toPromise: () => Promise<T>;
  /** Get value or throw error/promise */
  valueOrThrow: () => T;
  /** Get error or throw if not error state */
  errorOrThrow: () => any;
  /** Get promise or throw if not loading */
  promiseOrThrow: () => Promise<T>;
  /** Check equality with another loadable */
  is: (other: Loadable<any>) => boolean;
  /** Transform the loadable value */
  map: <S>(map: (from: T) => Loadable<S> | Promise<S> | S) => Loadable<S>;
}

interface ValueLoadable<T> extends BaseLoadable<T> {
  state: 'hasValue';
  contents: T;
  /** Get value if available, undefined otherwise */
  valueMaybe: () => T;
  /** Get error if available, undefined otherwise */
  errorMaybe: () => undefined;
  /** Get promise if available, undefined otherwise */
  promiseMaybe: () => undefined;
}

interface LoadingLoadable<T> extends BaseLoadable<T> {
  state: 'loading';
  contents: Promise<T>;
  valueMaybe: () => undefined;
  errorMaybe: () => undefined;
  promiseMaybe: () => Promise<T>;
}

interface ErrorLoadable<T> extends BaseLoadable<T> {
  state: 'hasError';
  contents: any;
  valueMaybe: () => undefined;
  errorMaybe: () => any;
  promiseMaybe: () => undefined;
}

Usage Examples:

import React from 'react';
import { useRecoilValueLoadable } from 'recoil';

// Component handling all loadable states
function AsyncDataDisplay({ dataState }) {
  const dataLoadable = useRecoilValueLoadable(dataState);
  
  switch (dataLoadable.state) {
    case 'hasValue':
      return <div>Data: {JSON.stringify(dataLoadable.contents)}</div>;
    
    case 'loading':
      return <div>Loading...</div>;
    
    case 'hasError':
      return <div>Error: {dataLoadable.contents.message}</div>;
  }
}

// Using loadable methods
function LoadableMethodsExample({ dataState }) {
  const dataLoadable = useRecoilValueLoadable(dataState);
  
  // Safe value access
  const value = dataLoadable.valueMaybe();
  const error = dataLoadable.errorMaybe();
  const promise = dataLoadable.promiseMaybe();
  
  return (
    <div>
      {value && <div>Value: {JSON.stringify(value)}</div>}
      {error && <div>Error: {error.message}</div>}
      {promise && <div>Loading...</div>}
    </div>
  );
}

// Transform loadable values
function TransformedLoadable({ userState }) {
  const userLoadable = useRecoilValueLoadable(userState);
  
  // Transform the loadable to get display name
  const displayNameLoadable = userLoadable.map(user => 
    user.displayName || user.email || 'Anonymous'
  );
  
  if (displayNameLoadable.state === 'hasValue') {
    return <div>Welcome, {displayNameLoadable.contents}!</div>;
  }
  
  return <div>Loading user...</div>;
}

RecoilLoadable Namespace

Factory functions and utilities for creating and working with loadables.

namespace RecoilLoadable {
  /**
   * Factory to make a Loadable object. If a Promise is provided the Loadable will
   * be in a 'loading' state until the Promise is either resolved or rejected.
   */
  function of<T>(x: T | Promise<T> | Loadable<T>): Loadable<T>;
  
  /**
   * Factory to make a Loadable object in an error state
   */
  function error(x: any): ErrorLoadable<any>;
  
  /**
   * Factory to make a loading Loadable which never resolves
   */
  function loading(): LoadingLoadable<any>;
  
  /**
   * Factory to make a Loadable which is resolved when all of the Loadables provided
   * to it are resolved or any one has an error. The value is an array of the values
   * of all of the provided Loadables. This is comparable to Promise.all() for Loadables.
   */
  function all<Inputs extends any[] | [Loadable<any>]>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
  function all<Inputs extends {[key: string]: any}>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
  
  /**
   * Returns true if the provided parameter is a Loadable type
   */
  function isLoadable(x: any): x is Loadable<any>;
}

type UnwrapLoadables<T extends any[] | { [key: string]: any }> = {
  [P in keyof T]: UnwrapLoadable<T[P]>;
};

type UnwrapLoadable<T> = T extends Loadable<infer R> ? R : T extends Promise<infer P> ? P : T;

Usage Examples:

import { RecoilLoadable, selector } from 'recoil';

// Create loadables from various inputs
const exampleSelector = selector({
  key: 'exampleSelector',
  get: () => {
    // From value
    const valueLoadable = RecoilLoadable.of('hello');
    
    // From promise
    const promiseLoadable = RecoilLoadable.of(
      fetch('/api/data').then(r => r.json())
    );
    
    // Error loadable
    const errorLoadable = RecoilLoadable.error(new Error('Something went wrong'));
    
    // Loading loadable that never resolves
    const loadingLoadable = RecoilLoadable.loading();
    
    return { valueLoadable, promiseLoadable, errorLoadable, loadingLoadable };
  },
});

// Combine multiple loadables
const combinedDataSelector = selector({
  key: 'combinedDataSelector',
  get: async ({get}) => {
    const userLoadable = get(noWait(userState));
    const settingsLoadable = get(noWait(settingsState));
    const preferencesLoadable = get(noWait(preferencesState));
    
    // Wait for all to resolve
    const combinedLoadable = RecoilLoadable.all([
      userLoadable,
      settingsLoadable,
      preferencesLoadable,
    ]);
    
    if (combinedLoadable.state === 'hasValue') {
      const [user, settings, preferences] = combinedLoadable.contents;
      return { user, settings, preferences };
    }
    
    // Propagate loading or error state
    return combinedLoadable.contents;
  },
});

// Combine object of loadables
const dashboardDataSelector = selector({
  key: 'dashboardDataSelector',
  get: async ({get}) => {
    const loadables = {
      user: get(noWait(userState)),
      posts: get(noWait(postsState)),
      notifications: get(noWait(notificationsState)),
    };
    
    const combinedLoadable = RecoilLoadable.all(loadables);
    
    if (combinedLoadable.state === 'hasValue') {
      return {
        ...combinedLoadable.contents,
        summary: `${combinedLoadable.contents.posts.length} posts, ${combinedLoadable.contents.notifications.length} notifications`,
      };
    }
    
    throw combinedLoadable.contents;
  },
});

// Type checking
function processUnknownValue(value: unknown) {
  if (RecoilLoadable.isLoadable(value)) {
    switch (value.state) {
      case 'hasValue':
        console.log('Loadable value:', value.contents);
        break;
      case 'loading':
        console.log('Loadable is loading');
        break;
      case 'hasError':
        console.log('Loadable error:', value.contents);
        break;
    }
  } else {
    console.log('Not a loadable:', value);
  }
}

Loadable Patterns

Common patterns for working with loadables in complex scenarios.

Usage Examples:

import React from 'react';
import { RecoilLoadable, selector, useRecoilValue } from 'recoil';

// Fallback chain with loadables
const dataWithFallbackSelector = selector({
  key: 'dataWithFallbackSelector',
  get: ({get}) => {
    const primaryLoadable = get(noWait(primaryDataState));
    const secondaryLoadable = get(noWait(secondaryDataState));
    const cacheLoadable = get(noWait(cacheDataState));
    
    // Try primary first
    if (primaryLoadable.state === 'hasValue') {
      return RecoilLoadable.of({
        data: primaryLoadable.contents,
        source: 'primary',
      });
    }
    
    // Try secondary
    if (secondaryLoadable.state === 'hasValue') {
      return RecoilLoadable.of({
        data: secondaryLoadable.contents,
        source: 'secondary',
      });
    }
    
    // Use cache as last resort
    if (cacheLoadable.state === 'hasValue') {
      return RecoilLoadable.of({
        data: cacheLoadable.contents,
        source: 'cache',
        stale: true,
      });
    }
    
    // All are loading or have errors
    if (primaryLoadable.state === 'loading' || 
        secondaryLoadable.state === 'loading') {
      return RecoilLoadable.loading();
    }
    
    // Return the primary error as it's most important
    return RecoilLoadable.error(primaryLoadable.contents);
  },
});

// Partial data accumulator
const partialDataSelector = selector({
  key: 'partialDataSelector',
  get: ({get}) => {
    const loadables = {
      essential: get(noWait(essentialDataState)),
      important: get(noWait(importantDataState)),
      optional: get(noWait(optionalDataState)),
    };
    
    const result = {
      essential: null,
      important: null,
      optional: null,
      status: 'partial',
    };
    
    // Must have essential data
    if (loadables.essential.state !== 'hasValue') {
      if (loadables.essential.state === 'hasError') {
        return RecoilLoadable.error(loadables.essential.contents);
      }
      return RecoilLoadable.loading();
    }
    
    result.essential = loadables.essential.contents;
    
    // Include other data if available
    if (loadables.important.state === 'hasValue') {
      result.important = loadables.important.contents;
    }
    
    if (loadables.optional.state === 'hasValue') {
      result.optional = loadables.optional.contents;
    }
    
    // Mark as complete if we have everything
    if (result.important && result.optional) {
      result.status = 'complete';  
    }
    
    return RecoilLoadable.of(result);
  },
});

// Loadable transformation chain
const processedDataSelector = selector({
  key: 'processedDataSelector',
  get: ({get}) => {
    const dataLoadable = get(noWait(rawDataState));
    
    // Chain transformations on the loadable
    return dataLoadable
      .map(data => data.filter(item => item.active))
      .map(data => data.map(item => ({
        ...item,
        displayName: item.name.toUpperCase(),
      })))
      .map(data => data.sort((a, b) => a.priority - b.priority));
  },
});

// Component with sophisticated loadable handling
function SmartDataComponent() {
  const dataLoadable = useRecoilValue(noWait(partialDataSelector));
  
  if (dataLoadable.state === 'loading') {
    return <div>Loading essential data...</div>;
  }
  
  if (dataLoadable.state === 'hasError') {
    return <div>Failed to load: {dataLoadable.contents.message}</div>;
  }
  
  const data = dataLoadable.contents;
  
  return (
    <div>
      <div>Essential: {JSON.stringify(data.essential)}</div>
      
      {data.important ? (
        <div>Important: {JSON.stringify(data.important)}</div>
      ) : (
        <div>Loading important data...</div>
      )}
      
      {data.optional ? (
        <div>Optional: {JSON.stringify(data.optional)}</div>
      ) : (
        <div>Optional data unavailable</div>
      )}
      
      <div>Status: {data.status}</div>
    </div>
  );
}

Error Handling Patterns

Graceful Degradation:

  • Use loadables to provide partial functionality when some data fails
  • Implement fallback chains for resilient data loading
  • Show appropriate loading states while preserving usability

Error Recovery:

  • Transform error loadables into default values where appropriate
  • Implement retry mechanisms using loadable state information
  • Provide user-friendly error messages with recovery options

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