The official, opinionated, batteries-included toolset for efficient Redux development
—
Redux Toolkit's async thunk functionality provides a streamlined approach to handling asynchronous logic with automatic loading states, error management, and type safety.
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';
});
}
});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()
})
}
);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());
}
}
);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()
})
}
);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;
});
}
});// 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]);
};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 };
}
);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