CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-reduxjs--toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

Pending
Overview
Eval results
Files

async-thunks.mddocs/

Async Operations

Redux Toolkit's async thunk functionality provides a streamlined approach to handling asynchronous logic with automatic loading states, error management, and type safety.

Capabilities

Create Async Thunk

Creates an async action creator that handles pending, fulfilled, and rejected states automatically.

/**
 * Creates async action creator for handling asynchronous logic
 * @param typePrefix - Base action type string (e.g., 'users/fetchById')
 * @param payloadCreator - Async function that returns the data or throws an error
 * @param options - Configuration options
 * @returns AsyncThunk with pending/fulfilled/rejected action creators
 */
function createAsyncThunk<
  Returned,
  ThunkArg = void,
  ThunkApiConfig extends AsyncThunkConfig = {}
>(
  typePrefix: string,
  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
  options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;

interface AsyncThunk<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> {
  /** Action creator for pending state */
  pending: ActionCreatorWithPreparedPayload<
    [string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?],
    undefined,
    string,
    never,
    { arg: ThunkArg; requestId: string; requestStatus: "pending" }
  >;
  
  /** Action creator for successful completion */
  fulfilled: ActionCreatorWithPreparedPayload<
    [Returned, string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?],
    Returned,
    string,
    never,
    { arg: ThunkArg; requestId: string; requestStatus: "fulfilled" }
  >;
  
  /** Action creator for rejection/error */
  rejected: ActionCreatorWithPreparedPayload<
    [unknown, string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?, string?, SerializedError?],
    undefined,
    string,
    SerializedError,
    { arg: ThunkArg; requestId: string; requestStatus: "rejected"; aborted?: boolean; condition?: boolean }
  >;
  
  /** Matcher for any settled action (fulfilled or rejected) */
  settled: ActionMatcher<ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']> | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>>;
  
  /** The base type prefix */
  typePrefix: string;
}

type AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (
  arg: ThunkArg,
  thunkAPI: GetThunkAPI<ThunkApiConfig>
) => Promise<Returned> | Returned;

interface AsyncThunkConfig {
  /** Return type of getState() */
  state?: unknown;
  /** Type of dispatch */
  dispatch?: Dispatch;
  /** Type of extra argument passed to thunk middleware */
  extra?: unknown;
  /** Return type of rejectWithValue's first argument */
  rejectValue?: unknown;
  /** Type passed into serializeError's first argument */
  serializedErrorType?: unknown;
  /** Type of pending meta's argument */
  pendingMeta?: unknown;
  /** Type of fulfilled meta's argument */
  fulfilledMeta?: unknown;
  /** Type of rejected meta's argument */
  rejectedMeta?: unknown;
}

Usage Examples:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// Basic async thunk
const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: string) => {
    const response = await userAPI.fetchById(userId);
    return response.data;
  }
);

// With error handling
const fetchUserByIdWithError = createAsyncThunk(
  'users/fetchByIdWithError',
  async (userId: string, { rejectWithValue }) => {
    try {
      const response = await userAPI.fetchById(userId);
      return response.data;
    } catch (err: any) {
      return rejectWithValue(err.response?.data || err.message);
    }
  }
);

// With state access and extra arguments
interface ThunkApiConfig {
  state: RootState;
  extra: { api: ApiClient; analytics: Analytics };
  rejectValue: { message: string; code?: number };
}

const fetchUserData = createAsyncThunk<
  User,
  string,
  ThunkApiConfig
>(
  'users/fetchData',
  async (userId, { getState, extra, rejectWithValue, signal }) => {
    const { user } = getState();
    
    // Check if already loading
    if (user.loading) {
      return rejectWithValue({ message: 'Already loading' });
    }
    
    // Use extra arguments
    const { api, analytics } = extra;
    analytics.track('user_fetch_started');
    
    try {
      const response = await api.fetchUser(userId, { signal });
      return response.data;
    } catch (err: any) {
      if (err.name === 'AbortError') {
        return rejectWithValue({ message: 'Request cancelled' });
      }
      return rejectWithValue({ 
        message: err.message,
        code: err.status 
      });
    }
  }
);

// Using in a slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    entities: {} as Record<string, User>,
    loading: false,
    error: null as string | null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.loading = false;
        state.entities[action.meta.arg] = action.payload;
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch user';
      });
  }
});

Async Thunk Options

Configure thunk behavior with conditional execution, custom error serialization, and metadata.

/**
 * Options for configuring async thunk behavior
 */
interface AsyncThunkOptions<ThunkArg, ThunkApiConfig extends AsyncThunkConfig> {
  /** Skip execution based on condition */
  condition?(arg: ThunkArg, api: GetThunkAPI<ThunkApiConfig>): boolean | undefined;
  
  /** Whether to dispatch rejected action when condition returns false */
  dispatchConditionRejection?: boolean;
  
  /** Custom error serialization */
  serializeError?(x: unknown): GetSerializedErrorType<ThunkApiConfig>;
  
  /** Custom request ID generation */
  idGenerator?(arg: ThunkArg): string;
  
  /** Add metadata to pending action */
  getPendingMeta?(
    base: { arg: ThunkArg; requestId: string },
    api: GetThunkAPI<ThunkApiConfig>
  ): GetPendingMeta<ThunkApiConfig>;
  
  /** Add metadata to fulfilled action */
  getFulfilledMeta?(
    base: { arg: ThunkArg; requestId: string },
    api: GetThunkAPI<ThunkApiConfig>
  ): GetFulfilledMeta<ThunkApiConfig>;
  
  /** Add metadata to rejected action */
  getRejectedMeta?(
    base: { arg: ThunkArg; requestId: string },
    api: GetThunkAPI<ThunkApiConfig>
  ): GetRejectedMeta<ThunkApiConfig>;
}

Usage Examples:

// Conditional execution
const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: string) => {
    return await userAPI.fetchById(userId);
  },
  {
    // Skip if user already exists
    condition: (userId, { getState }) => {
      const { users } = getState() as RootState;
      return !users.entities[userId];
    },
    
    // Custom ID generation
    idGenerator: (userId) => `user-${userId}-${Date.now()}`,
    
    // Add metadata to pending action
    getPendingMeta: ({ arg }, { getState }) => ({
      startedTimeStamp: Date.now(),
      source: 'user-component'
    })
  }
);

// Custom error serialization
const fetchWithCustomErrors = createAsyncThunk(
  'data/fetch',
  async (id: string) => {
    throw new Error('API Error');
  },
  {
    serializeError: (error: any) => ({
      message: error.message,
      code: error.code || 'UNKNOWN',
      timestamp: Date.now()
    })
  }
);

Thunk API Object

The thunk API object provides access to Redux store methods and additional utilities.

/**
 * ThunkAPI object passed to async thunk payload creators
 */
interface BaseThunkAPI<S, E, D extends Dispatch, RejectedValue, RejectedMeta, FulfilledMeta> {
  /** Function to get current state */
  getState(): S;
  
  /** Enhanced dispatch function */
  dispatch: D;
  
  /** Extra argument from thunk middleware */
  extra: E;
  
  /** Unique request identifier */
  requestId: string;
  
  /** AbortSignal for request cancellation */
  signal: AbortSignal;
  
  /** Reject with custom value */
  rejectWithValue(value: RejectedValue, meta?: RejectedMeta): RejectWithValue<RejectedValue, RejectedMeta>;
  
  /** Fulfill with custom value */
  fulfillWithValue<FulfilledValue>(value: FulfilledValue, meta?: FulfilledMeta): FulfillWithMeta<FulfilledValue, FulfilledMeta>;
}

type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
  GetState<ThunkApiConfig>,
  GetExtra<ThunkApiConfig>,
  GetDispatch<ThunkApiConfig>,
  GetRejectValue<ThunkApiConfig>,
  GetRejectedMeta<ThunkApiConfig>,
  GetFulfilledMeta<ThunkApiConfig>
>;

Usage Examples:

const complexAsyncThunk = createAsyncThunk(
  'complex/operation',
  async (
    { id, data }: { id: string; data: any },
    { getState, dispatch, extra, requestId, signal, rejectWithValue, fulfillWithValue }
  ) => {
    const state = getState() as RootState;
    
    // Check current state
    if (state.complex.processing) {
      return rejectWithValue('Already processing');
    }
    
    // Dispatch other actions
    dispatch(startProcessing());
    
    // Use extra arguments (API client, etc.)
    const { apiClient } = extra as { apiClient: ApiClient };
    
    try {
      // Check for cancellation
      if (signal.aborted) {
        throw new Error('Operation cancelled');
      }
      
      const result = await apiClient.processData(id, data, { signal });
      
      // Return with custom metadata
      return fulfillWithValue(result, {
        requestId,
        processedAt: Date.now()
      });
    } catch (error: any) {
      if (error.name === 'AbortError') {
        return rejectWithValue('Cancelled', { reason: 'user_cancelled' });
      }
      return rejectWithValue(error.message, { errorCode: error.code });
    } finally {
      dispatch(endProcessing());
    }
  }
);

Result Unwrapping

Utilities for working with async thunk results and handling fulfilled/rejected outcomes.

/**
 * Unwraps the result of an async thunk action
 * @param action - The dispatched async thunk action
 * @returns Promise that resolves with payload or rejects with error
 */
function unwrapResult<T>(action: { payload: T } | { error: SerializedError | any }): T;

/**
 * Serializes Error objects to plain objects
 * @param value - Error or other value to serialize
 * @returns Serialized error object
 */
function miniSerializeError(value: any): SerializedError;

interface SerializedError {
  name?: string;
  message?: string;
  code?: string;
  stack?: string;
}

Usage Examples:

import { unwrapResult } from '@reduxjs/toolkit';

// Component usage with unwrapResult
const UserProfile = () => {
  const dispatch = useAppDispatch();
  const [loading, setLoading] = useState(false);
  
  const handleFetchUser = async (userId: string) => {
    setLoading(true);
    try {
      // unwrapResult will throw if the thunk was rejected
      const user = await dispatch(fetchUserById(userId)).then(unwrapResult);
      console.log('User loaded:', user);
      // Handle success
    } catch (error) {
      console.error('Failed to load user:', error);
      // Handle error
    } finally {
      setLoading(false);
    }
  };
  
  // ... component JSX
};

// Using with async/await in thunks
const complexOperation = createAsyncThunk(
  'complex/operation',
  async (data, { dispatch }) => {
    try {
      // Chain multiple async thunks
      const user = await dispatch(fetchUserById(data.userId)).then(unwrapResult);
      const settings = await dispatch(fetchUserSettings(user.id)).then(unwrapResult);
      
      return { user, settings };
    } catch (error) {
      // Handle any of the chained operations failing
      throw error; // Will be caught by thunk and trigger rejected action
    }
  }
);

// Custom error serialization
const customErrorThunk = createAsyncThunk(
  'data/fetchWithCustomError',
  async (id: string) => {
    throw new CustomError('Something went wrong', 'CUSTOM_ERROR_CODE');
  },
  {
    serializeError: (err: any) => ({
      ...miniSerializeError(err),
      customCode: err.code,
      timestamp: Date.now()
    })
  }
);

Action Matchers

Redux Toolkit provides action matchers for handling async thunk actions in reducers.

/**
 * Matches pending actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher function
 */
function isPending(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<PendingAction<any>>;

/**
 * Matches fulfilled actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher function
 */
function isFulfilled(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<FulfilledAction<any, any>>;

/**
 * Matches rejected actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher function
 */
function isRejected(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedAction<any, any>>;

/**
 * Matches any action from async thunks (pending, fulfilled, or rejected)
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher function
 */
function isAsyncThunkAction(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<AnyAsyncThunkAction>;

/**
 * Matches rejected actions that used rejectWithValue
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher function
 */
function isRejectedWithValue(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedWithValueAction<any, any>>;

Usage Examples:

import { 
  isPending, 
  isFulfilled, 
  isRejected, 
  isAsyncThunkAction,
  isRejectedWithValue 
} from '@reduxjs/toolkit';

const dataSlice = createSlice({
  name: 'data',
  initialState: {
    loading: false,
    error: null,
    users: {},
    posts: {}
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      // Handle all pending states
      .addMatcher(
        isPending(fetchUserById, fetchPosts, updateUser),
        (state) => {
          state.loading = true;
          state.error = null;
        }
      )
      // Handle all fulfilled states
      .addMatcher(
        isFulfilled(fetchUserById, fetchPosts, updateUser),
        (state) => {
          state.loading = false;
        }
      )
      // Handle rejected states with custom values
      .addMatcher(
        isRejectedWithValue(fetchUserById, updateUser),
        (state, action) => {
          state.loading = false;
          state.error = action.payload; // Custom error from rejectWithValue
        }
      )
      // Handle other rejected states
      .addMatcher(
        isRejected(fetchUserById, fetchPosts, updateUser),
        (state, action) => {
          state.loading = false;
          state.error = action.error.message || 'Something went wrong';
        }
      );
  }
});

// Generic loading handler for multiple thunks
const createLoadingSlice = (thunks: AsyncThunk<any, any, any>[]) =>
  createSlice({
    name: 'loading',
    initialState: { isLoading: false },
    reducers: {},
    extraReducers: (builder) => {
      builder
        .addMatcher(isPending(...thunks), (state) => {
          state.isLoading = true;
        })
        .addMatcher(isFulfilled(...thunks), (state) => {
          state.isLoading = false;
        })
        .addMatcher(isRejected(...thunks), (state) => {
          state.isLoading = false;
        });
    }
  });

Advanced Patterns

Request Cancellation

// Thunk with cancellation support
const cancellableThunk = createAsyncThunk(
  'data/fetch',
  async (params, { signal }) => {
    const response = await fetch('/api/data', { signal });
    return response.json();
  }
);

// Component with cancellation
const DataComponent = () => {
  const dispatch = useAppDispatch();
  const promiseRef = useRef<any>();
  
  useEffect(() => {
    promiseRef.current = dispatch(cancellableThunk(params));
    
    return () => {
      promiseRef.current?.abort();
    };
  }, [dispatch, params]);
};

Thunk Chaining

const chainedOperation = createAsyncThunk(
  'data/chainedOperation',
  async (userId: string, { dispatch, getState }) => {
    // Chain multiple async operations
    const user = await dispatch(fetchUserById(userId)).then(unwrapResult);
    const profile = await dispatch(fetchUserProfile(user.id)).then(unwrapResult);
    const permissions = await dispatch(fetchUserPermissions(user.id)).then(unwrapResult);
    
    return { user, profile, permissions };
  }
);

Optimistic Updates

const updateUserOptimistic = createAsyncThunk(
  'users/updateOptimistic',
  async (
    { id, updates }: { id: string; updates: Partial<User> },
    { dispatch, rejectWithValue }
  ) => {
    // Optimistically apply update
    dispatch(userUpdatedOptimistically({ id, updates }));
    
    try {
      const updatedUser = await userAPI.update(id, updates);
      return { id, user: updatedUser };
    } catch (error: any) {
      // Revert optimistic update on failure
      dispatch(revertOptimisticUpdate(id));
      return rejectWithValue(error.message);
    }
  }
);

Install with Tessl CLI

npx tessl i tessl/npm-reduxjs--toolkit

docs

actions-reducers.md

async-thunks.md

core-store.md

entity-adapters.md

index.md

middleware.md

react-integration.md

rtk-query-react.md

rtk-query.md

utilities.md

tile.json