createAsyncThunk
Overview#
A function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
This abstracts the standard recommended approach for handling async request lifecycles.
It does not generate any reducer functions, since it does not know what data you're fetching, how you want to track loading state, or how the data you return needs to be processed. You should write your own reducer logic that handles these actions, with whatever loading state and processing logic is appropriate for your own app.
tip
Redux Toolkit's RTK Query data fetching API is a purpose built data fetching and caching solution for Redux apps, and can eliminate the need to write any thunks or reducers to manage data fetching. We encourage you to try it out and see if it can help simplify the data fetching code in your own apps!
Sample usage:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'import { userAPI } from './userAPI'
// First, create the thunkconst fetchUserById = createAsyncThunk( 'users/fetchByIdStatus', async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId) return response.data })
// Then, handle actions in your reducers:const usersSlice = createSlice({ name: 'users', initialState: { entities: [], loading: 'idle' }, reducers: { // standard reducer logic, with auto-generated action types per reducer }, extraReducers: (builder) => { // Add reducers for additional action types here, and handle loading state as needed builder.addCase(fetchUserById.fulfilled, (state, action) => { // Add user to the state array state.entities.push(action.payload) }) },})
// Later, dispatch the thunk as needed in the appdispatch(fetchUserById(123))Parameters#
createAsyncThunk accepts three parameters: a string action type value, a payloadCreator callback, and an options object.
type#
A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request:
For example, a type argument of 'users/requestStatus' will generate these action types:
pending:'users/requestStatus/pending'fulfilled:'users/requestStatus/fulfilled'rejected:'users/requestStatus/rejected'
payloadCreator#
A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an Error instance or a plain value such as a descriptive error message or otherwise a resolved promise with a RejectWithValue argument as returned by the thunkAPI.rejectWithValue function.
The payloadCreator function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native AsyncStorage, and so on.
The payloadCreator function will be called with two arguments:
arg: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. This is useful for passing in values like item IDs that may be needed as part of the request. If you need to pass in multiple values, pass them together in an object when you dispatch the thunk, likedispatch(fetchUsers({status: 'active', sortBy: 'name'})).thunkAPI: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options:dispatch: the Redux storedispatchmethodgetState: the Redux storegetStatemethodextra: the "extra argument" given to the thunk middleware on setup, if availablerequestId: a unique string ID value that was automatically generated to identify this request sequencesignal: anAbortController.signalobject that may be used to see if another part of the app logic has marked this request as needing cancelation.rejectWithValue(value, [meta]): rejectWithValue is a utility function that you canreturn(orthrow) in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action. If you also pass in ameta, it will be merged with the existingrejectedAction.meta.fulfillWithValue(value, meta): fulfillWithValue is a utility function that you canreturnin your action creator tofulfillwith a value while having the ability of adding tofulfilledAction.meta.
The logic in the payloadCreator function may use any of these values as needed to calculate the result.
Options#
An object with the following optional fields:
condition(arg, { getState, extra } ): boolean: a callback that can be used to skip execution of the payload creator and all action dispatches, if desired. See Canceling Before Execution for a complete description.dispatchConditionRejection: ifcondition()returnsfalse, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, set this flag totrue.idGenerator(): string: a function to use when generating therequestIdfor the request sequence. Defaults to use nanoid.serializeError(error: unknown) => anyto replace the internalminiSerializeErrormethod with your own serialization logic.getPendingMeta({ arg, requestId }, { getState, extra }): any: a function to create an object that will be merged into thependingAction.metafield.
Return Value#
createAsyncThunk returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the pending, fulfilled, and rejected cases attached as nested fields.
Using the fetchUserById example above, createAsyncThunk will generate four functions:
fetchUserById, the thunk action creator that kicks off the async payload callback you wrotefetchUserById.pending, an action creator that dispatches an'users/fetchByIdStatus/pending'actionfetchUserById.fulfilled, an action creator that dispatches an'users/fetchByIdStatus/fulfilled'actionfetchUserById.rejected, an action creator that dispatches an'users/fetchByIdStatus/rejected'action
When dispatched, the thunk will:
- dispatch the
pendingaction - call the
payloadCreatorcallback and wait for the returned promise to settle - when the promise settles:
- if the promise resolved successfully, dispatch the
fulfilledaction with the promise value asaction.payload - if the promise resolved with a
rejectWithValue(value)return value, dispatch therejectedaction with the value passed intoaction.payloadand 'Rejected' asaction.error.message - if the promise failed and was not handled with
rejectWithValue, dispatch therejectedaction with a serialized version of the error value asaction.error
- if the promise resolved successfully, dispatch the
- Return a fulfilled promise containing the final dispatched action (either the
fulfilledorrejectedaction object)
Promise Lifecycle Actions#
createAsyncThunk will generate three Redux action creators using createAction: pending, fulfilled, and rejected. Each lifecycle action creator will be attached to the returned thunk action creator so that your reducer logic can reference the action types and respond to the actions when dispatched. Each action object will contain the current unique requestId and arg values under action.meta.
The action creators will have these signatures:
interface SerializedError { name?: string message?: string code?: string stack?: string}
interface PendingAction<ThunkArg> { type: string payload: undefined meta: { requestId: string arg: ThunkArg }}
interface FulfilledAction<ThunkArg, PromiseResult> { type: string payload: PromiseResult meta: { requestId: string arg: ThunkArg }}
interface RejectedAction<ThunkArg> { type: string payload: undefined error: SerializedError | any meta: { requestId: string arg: ThunkArg aborted: boolean condition: boolean }}
interface RejectedWithValueAction<ThunkArg, RejectedValue> { type: string payload: RejectedValue error: { message: 'Rejected' } meta: { requestId: string arg: ThunkArg aborted: boolean }}
type Pending = <ThunkArg>( requestId: string, arg: ThunkArg) => PendingAction<ThunkArg>
type Fulfilled = <ThunkArg, PromiseResult>( payload: PromiseResult, requestId: string, arg: ThunkArg) => FulfilledAction<ThunkArg, PromiseResult>
type Rejected = <ThunkArg>( requestId: string, arg: ThunkArg) => RejectedAction<ThunkArg>
type RejectedWithValue = <ThunkArg, RejectedValue>( requestId: string, arg: ThunkArg) => RejectedWithValueAction<ThunkArg, RejectedValue>To handle these actions in your reducers, reference the action creators in createReducer or createSlice using either the object key notation or the "builder callback" notation. (Note that if you use TypeScript, you should use the "builder callback" notation to ensure the types are inferred correctly):
const reducer1 = createReducer(initialState, { [fetchUserById.fulfilled]: (state, action) => {},})
const reducer2 = createReducer(initialState, (builder) => { builder.addCase(fetchUserById.fulfilled, (state, action) => {})})
const reducer3 = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: { [fetchUserById.fulfilled]: (state, action) => {}, },})
const reducer4 = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: (builder) => { builder.addCase(fetchUserById.fulfilled, (state, action) => {}) },})Handling Thunk Results#
Unwrapping Result Actions#
Thunks may return a value when dispatched. A common use case is to return a promise from the thunk, dispatch the thunk from a component, and then wait for the promise to resolve before doing additional work:
const onClick = () => { dispatch(fetchUserById(userId)).then(() => { // do additional work })}The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. The promise returned by the dispatched thunk has an unwrap property which can be called to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
// in the component
const onClick = () => { dispatch(fetchUserById(userId)) .unwrap() .then((originalPromiseResult) => { // handle result here }) .catch((rejectedValueOrSerializedError) => { // handle error here })}Or with async/await syntax:
// in the component
const onClick = async () => { try { const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap() // handle result here } catch (rejectedValueOrSerializedError) { // handle error here }}Using the attached .unwrap() property is preferred in most cases, however Redux Toolkit also exports an unwrapResult function that can be used for a similar purpose:
import { unwrapResult } from '@reduxjs/toolkit'
// in the componentconst onClick = () => { dispatch(fetchUserById(userId)) .then(unwrapResult) .then((originalPromiseResult) => { // handle result here }) .catch((rejectedValueOrSerializedError) => { // handle result here })}Or with async/await syntax:
import { unwrapResult } from '@reduxjs/toolkit'
// in the componentconst onClick = async () => { try { const resultAction = await dispatch(fetchUserById(userId)) const originalPromiseResult = unwrapResult(resultAction) // handle result here } catch (rejectedValueOrSerializedError) { // handle error here }}Checking Errors After Dispatching#
Note that this means a failed request or error in a thunk will never return a rejected promise. We assume that any failure is more of a handled error than an unhandled exception at this point. This is due to the fact that we want to prevent uncaught promise rejections for those who do not use the result of dispatch.
If your component needs to know if the request failed, use .unwrap or unwrapResult and handle the re-thrown error accordingly.
Handling Thunk Errors#
When your payloadCreator returns a rejected promise (such as a thrown error in an async function), the thunk will dispatch a rejected action containing an automatically-serialized version of the error as action.error. However, to ensure serializability, everything that does not match the SerializedError interface will have been removed from it:
export interface SerializedError { name?: string message?: string stack?: string code?: string}If you need to customize the contents of the rejected action, you should catch any errors yourself, and then return a new value using the thunkAPI.rejectWithValue utility. Doing return rejectWithValue(errorPayload) will cause the rejected action to use that value as action.payload.
The rejectWithValue approach should also be used if your API response "succeeds", but contains some kind of additional error details that the reducer should know about. This is particularly common when expecting field-level validation errors from an API.
const updateUser = createAsyncThunk( 'users/update', async (userData, { rejectWithValue }) => { const { id, ...fields } = userData try { const response = await userAPI.updateById(id, fields) return response.data.user } catch (err) { // Use `err.response.data` as `action.payload` for a `rejected` action, // by explicitly returning it using the `rejectWithValue()` utility return rejectWithValue(err.response.data) } })Cancellation#
Canceling Before Execution#
If you need to cancel a thunk before the payload creator is called, you may provide a condition callback as an option after the payload creator. The callback will receive the thunk argument and an object with {getState, extra} as parameters, and use those to decide whether to continue or not. If the execution should be canceled, the condition callback should return a literal false value:
const fetchUserById = createAsyncThunk( 'users/fetchByIdStatus', async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId) return response.data }, { condition: (userId, { getState, extra }) => { const { users } = getState() const fetchStatus = users.requests[userId] if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') { // Already fetched or in progress, don't need to re-fetch return false } }, })If condition() returns false, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, pass in {condition, dispatchConditionRejection: true}.
Canceling While Running#
If you want to cancel your running thunk before it has finished, you can use the abort method of the promise returned by dispatch(fetchUserById(userId)).
A real-life example of that would look like this:
- TypeScript
- JavaScript
import { fetchUserById } from './slice'import { useAppDispatch } from './store'import React from 'react'
function MyComponent(props: { userId: string }) { const dispatch = useAppDispatch() React.useEffect(() => { // Dispatching the thunk returns a promise const promise = dispatch(fetchUserById(props.userId)) return () => { // `createAsyncThunk` attaches an `abort()` method to the promise promise.abort() } }, [props.userId])}import { fetchUserById } from './slice'import { useAppDispatch } from './store'import React from 'react'
function MyComponent(props) { const dispatch = useAppDispatch() React.useEffect(() => { // Dispatching the thunk returns a promise const promise = dispatch(fetchUserById(props.userId)) return () => { // `createAsyncThunk` attaches an `abort()` method to the promise promise.abort() } }, [props.userId])}After a thunk has been cancelled this way, it will dispatch (and return) a "thunkName/rejected" action with an AbortError on the error property. The thunk will not dispatch any further actions.
Additionally, your payloadCreator can use the AbortSignal it is passed via thunkAPI.signal to actually cancel a costly asynchronous action.
The fetch api of modern browsers already comes with support for an AbortSignal:
- TypeScript
- JavaScript
import { createAsyncThunk } from '@reduxjs/toolkit'
const fetchUserById = createAsyncThunk( 'users/fetchById', async (userId: string, thunkAPI) => { const response = await fetch(`https://reqres.in/api/users/${userId}`, { signal: thunkAPI.signal, }) return await response.json() })import { createAsyncThunk } from '@reduxjs/toolkit'
const fetchUserById = createAsyncThunk( 'users/fetchById', async (userId, thunkAPI) => { const response = await fetch(`https://reqres.in/api/users/${userId}`, { signal: thunkAPI.signal, }) return await response.json() })Checking Cancellation Status#
Reading the Signal Value#
You can use the signal.aborted property to regularly check if the thunk has been aborted and in that case stop costly long-running work:
- TypeScript
- JavaScript
import { createAsyncThunk } from '@reduxjs/toolkit'
const readStream = createAsyncThunk( 'readStream', async (stream: ReadableStream, { signal }) => { const reader = stream.getReader()
let done = false let result = ''
while (!done) { if (signal.aborted) { throw new Error('stop the work, this has been aborted!') } const read = await reader.read() result += read.value done = read.done } return result })import { createAsyncThunk } from '@reduxjs/toolkit'
const readStream = createAsyncThunk( 'readStream', async (stream, { signal }) => { const reader = stream.getReader()
let done = false let result = ''
while (!done) { if (signal.aborted) { throw new Error('stop the work, this has been aborted!') } const read = await reader.read() result += read.value done = read.done } return result })Listening for Abort Events#
You can also call signal.addEventListener('abort', callback) to have logic inside the thunk be notified when promise.abort() was called.
This can for example be used in conjunction with an axios CancelToken:
- TypeScript
- JavaScript
import { createAsyncThunk } from '@reduxjs/toolkit'import axios from 'axios'
const fetchUserById = createAsyncThunk( 'users/fetchById', async (userId, { signal }) => { const source = axios.CancelToken.source() signal.addEventListener('abort', () => { source.cancel() }) const response = await axios.get(`https://reqres.in/api/users/${userId}`, { cancelToken: source.token, }) return response.data })import { createAsyncThunk } from '@reduxjs/toolkit'import axios from 'axios'
const fetchUserById = createAsyncThunk( 'users/fetchById', async (userId, { signal }) => { const source = axios.CancelToken.source() signal.addEventListener('abort', () => { source.cancel() }) const response = await axios.get(`https://reqres.in/api/users/${userId}`, { cancelToken: source.token, }) return response.data })Examples#
- Requesting a user by ID, with loading state, and only one request at a time:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk( 'users/fetchByIdStatus', async (userId, { getState, requestId }) => { const { currentRequestId, loading } = getState().users if (loading !== 'pending' || requestId !== currentRequestId) { return } const response = await userAPI.fetchById(userId) return response.data })
const usersSlice = createSlice({ name: 'users', initialState: { entities: [], loading: 'idle', currentRequestId: undefined, error: null, }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUserById.pending, (state, action) => { if (state.loading === 'idle') { state.loading = 'pending' state.currentRequestId = action.meta.requestId } }) .addCase(fetchUserById.fulfilled, (state, action) => { const { requestId } = action.meta if ( state.loading === 'pending' && state.currentRequestId === requestId ) { state.loading = 'idle' state.entities.push(action.payload) state.currentRequestId = undefined } }) .addCase(fetchUserById.rejected, (state, action) => { const { requestId } = action.meta if ( state.loading === 'pending' && state.currentRequestId === requestId ) { state.loading = 'idle' state.error = action.error state.currentRequestId = undefined } }) },})
const UsersComponent = () => { const { users, loading, error } = useSelector((state) => state.users) const dispatch = useDispatch()
const fetchOneUser = async (userId) => { try { const user = await dispatch(fetchUserById(userId)).unwrap() showToast('success', `Fetched ${user.name}`) } catch (err) { showToast('error', `Fetch failed: ${err.message}`) } }
// render UI here}Using rejectWithValue to access a custom rejected payload in a component
Note: this is a contrived example assuming our userAPI only ever throws validation-specific errors
- TypeScript
- JavaScript
// file: user/slice.tsimport { createAsyncThunk, createSlice } from '@reduxjs/toolkit'import { userAPI } from './userAPI'import { AxiosError } from 'axios'
// Sample types that will be usedexport interface User { id: string first_name: string last_name: string email: string}
interface ValidationErrors { errorMessage: string field_errors: Record<string, string>}
interface UpdateUserResponse { user: User success: boolean}
export const updateUser = createAsyncThunk< User, { id: string } & Partial<User>, { rejectValue: ValidationErrors }>('users/update', async (userData, { rejectWithValue }) => { try { const { id, ...fields } = userData const response = await userAPI.updateById<UpdateUserResponse>(id, fields) return response.data.user } catch (err) { let error: AxiosError<ValidationErrors> = err // cast the error for access if (!error.response) { throw err } // We got validation errors, let's return those so we can reference in our component and set form errors return rejectWithValue(error.response.data) }})
interface UsersState { error: string | null | undefined entities: Record<string, User>}
const initialState = { entities: {}, error: null,} as UsersState
const usersSlice = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: (builder) => { // The `builder` callback form is used here because it provides correctly typed reducers from the action creators builder.addCase(updateUser.fulfilled, (state, { payload }) => { state.entities[payload.id] = payload }) builder.addCase(updateUser.rejected, (state, action) => { if (action.payload) { // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here. state.error = action.payload.errorMessage } else { state.error = action.error.message } }) },})
export default usersSlice.reducer
// file: user/UsersComponent.tsimport React from 'react'import { useAppDispatch, RootState } from '../store'import { useSelector } from 'react-redux'import { User, updateUser } from './slice'import { FormikHelpers } from 'formik'import { showToast } from 'some-toast-library'
interface FormValues extends Omit<User, 'id'> {}
const UsersComponent = (props: { id: string }) => { const { entities, error } = useSelector((state: RootState) => state.users) const dispatch = useAppDispatch()
// This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action const handleUpdateUser = async ( values: FormValues, formikHelpers: FormikHelpers<FormValues> ) => { const resultAction = await dispatch(updateUser({ id: props.id, ...values })) if (updateUser.fulfilled.match(resultAction)) { // user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk const user = resultAction.payload showToast('success', `Updated ${user.first_name} ${user.last_name}`) } else { if (resultAction.payload) { // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here. formikHelpers.setErrors(resultAction.payload.field_errors) } else { showToast('error', `Update failed: ${resultAction.error}`) } } }
// render UI here}// file: user/slice.tsimport { createAsyncThunk, createSlice } from '@reduxjs/toolkit'import { userAPI } from './userAPI'
export const updateUser = createAsyncThunk( 'users/update', async (userData, { rejectWithValue }) => { try { const { id, ...fields } = userData const response = await userAPI.updateById(id, fields) return response.data.user } catch (err) { let error = err // cast the error for access if (!error.response) { throw err } // We got validation errors, let's return those so we can reference in our component and set form errors return rejectWithValue(error.response.data) } })
const initialState = { entities: {}, error: null,}
const usersSlice = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: (builder) => { // The `builder` callback form is used here because it provides correctly typed reducers from the action creators builder.addCase(updateUser.fulfilled, (state, { payload }) => { state.entities[payload.id] = payload }) builder.addCase(updateUser.rejected, (state, action) => { if (action.payload) { // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here. state.error = action.payload.errorMessage } else { state.error = action.error.message } }) },})
export default usersSlice.reducer
// file: user/UsersComponent.tsimport { useAppDispatch } from '../store'import { useSelector } from 'react-redux'import { updateUser } from './slice'import { showToast } from 'some-toast-library'
const UsersComponent = (props) => { const { entities, error } = useSelector((state) => state.users) const dispatch = useAppDispatch()
// This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action const handleUpdateUser = async (values, formikHelpers) => { const resultAction = await dispatch(updateUser({ id: props.id, ...values })) if (updateUser.fulfilled.match(resultAction)) { // user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk const user = resultAction.payload showToast('success', `Updated ${user.first_name} ${user.last_name}`) } else { if (resultAction.payload) { // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here. formikHelpers.setErrors(resultAction.payload.field_errors) } else { showToast('error', `Update failed: ${resultAction.error}`) } } }
// render UI here}