useAsyncState

The `useAsyncState` hook provides a robust and scalable solution for managing the asynchronous lifecycle of actions (API calls, complex calculations, etc.) within React functional components. By utilizing `useReducer` and a clear state machine, it encapsulates loading, success, and error handling into a reusable and declarative API.

Hook Signature

/**
 * @param {string} initialStatus - Optional initial status for the action. Defaults to "idle".
 * @returns {object} The action state API.
 */
function useAsyncState(initialStatus: Status = 'idle'): ActionStateAPI
ParameterTypeDefaultDescription
initialStatusstring"idle"The starting state of the hook: "idle", "pending", "success", or "error".

Return API (ActionStateAPI)

The hook returns an object containing state variables, action dispatchers, and declarative rendering components.

State Properties

PropertyTypeDescription
dataanyThe payload returned by the successful asynchronous callback. Null otherwise.
erroranyThe error payload (typically a string message) captured during a failed action. Null otherwise.
statusstringThe current state: "idle", "pending", "success", or "error".
isPendingbooleantrue if the action is currently executing.
isErrorbooleantrue if the action completed with an error.
isSuccessbooleantrue if the action completed successfully.

Action Functions

FunctionSignatureDescription
startAction(callback: () => Promise<T>) => Promise<T>Executes the provided asynchronous function. Updates status to "pending" before execution and to "success" or "error" after completion.
reset() => voidResets the state (data and error) and sets status back to "idle".

Conditional Rendering Components

The hook provides a Unified Renderer for a low-code approach, along with Granular Renders for fine-grained control.

ComponentUsage PatternDescription
RendererUnified, Low-CodeA single component that accepts idle, pending, success, and error props to map the entire state flow declaratively.
RenderPendingGranularRenders children only when isPending is true.
RenderSuccessGranularRenders children only on isSuccess. Supports a function-as-child pattern to pass data.
RenderErrorGranularRenders children only on isError. Supports a function-as-child pattern to pass error.

Code Usage

This section demonstrates a React component that fetches user data from JSONPlaceholder.

1. Example Without useAsyncState

This approach requires manual management of three separate state variables and explicit conditional logic in the JSX.

import React, { useState, useCallback } from 'react';

const fetchUser = async (id) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
  return response.json();
};

function TraditionalFetcher({ userId = 1 }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const startFetch = useCallback(async () => {
    setIsPending(true);
    setError(null);
    setData(null);

    try {
      const user = await fetchUser(userId);
      setData(user);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsPending(false);
    }
  }, [userId]);
  
  const reset = () => {
      setData(null);
      setError(null);
      setIsPending(false);
  };

  return (
    <div>
      <h3>1. Traditional Fetcher (High-Code)</h3>
      <button onClick={startFetch} disabled={isPending}>
        {isPending ? 'Fetching...' : 'Fetch User'}
      </button>
      <button onClick={reset}>Reset</button>

      {/* Manual Conditional Logic */}
      {isPending && <p>Loading user data...</p>}

      {error && <p>Error: {error}</p>}

      {data && (
        <div>
          <h4>User: {data.name}</h4>
          <p>Email: {data.email}</p>
        </div>
      )}
    </div>
  );
}

2. Example With useAsyncState

This approach uses the unified Renderer for a declarative, low-code component structure. All state management and conditional rendering logic are delegated to the hook.

import React from 'react';
// import { useAsyncState } from './useAsyncState'; // Assume hook import

// NOTE: useAsyncState and fetchUser are assumed to be available.

function HookFetcher({ userId = 1 }) {
  // 1. Destructure the action functions and the unified renderer
  const { 
    startAction, 
    reset, 
    Renderer,
    status 
  } = useAsyncState();

  const handleFetch = () => startAction(() => fetchUser(userId));

  return (
    <div>
      <h3>2. Hook Fetcher (Low-Code)</h3>
      <button onClick={handleFetch} disabled={status === 'pending'}>
        Fetch User
      </button>
      <button onClick={reset}>Reset</button>
      
      {/* Declarative, Low-Code Rendering */}
      <Renderer
        idle={<p>Click to fetch user data.</p>}
        
        pending={
          <p>Loading user data...</p>
        }
        
        success={(user) => (
          <div>
            <h4>User: {user.name}</h4>
            <p>Email: {user.email}</p>
          </div>
        )}
        
        error={(errorMsg) => (
          <p>Error: {errorMsg}</p>
        )}
      />
    </div>
  );
}