ReduxJS/Toolkit: createAsyncThunk
posted in javascript on • by Wouter Van Schandevijl •createAsyncThunk
generates promise lifecycle action types, which you
handle in the extraReducers
of your slice.
import { createAsyncThunk } from "@reduxjs/toolkit";
export const fetchStuff = createAsyncThunk(
'resource/action',
async (params, thunkApi) => {
try {
const response = await fetch(`/api/resource/${params.id}`);
return await response.json();
} catch {
// Also possible to return a rejected Promise
return thunkApi.rejectWithValue("Well, that failed");
}
}
);
This dispatches the following actions (from the first “Type” argument):
resource/action/pending
: display a spinner or something?resource/action/rejected
: display an error message?resource/action/fulfilled
: while the previous two could be considered optional, you definitely want to handle this one!
Check Promise Lifecycle Actions for the exact signatures of the pending/rejected/fulfilled actions.
Reduce It
const mySlice = createSlice({
extraReducers: builder => {
builder.addCase(fetchStuff.fulfilled, (state, action) => {
// action.payload is what fetchStuff returned
});
builder.addCase(fetchStuff.pending, (state, action) => {});
builder.addCase(fetchStuff.rejected, (state, action) => {
// action.payload === 'Well, that failed'
// action.error.message === 'Rejected'
});
builder.addMatcher(fetchStuff.settled, (state, action) => {
// Called for both fulfilled and rejected
});
}
});
Parameters
createAsyncThunk(Type, PayloadCreator, [Options]);
1. Type
A string that will be prepended with the three states and dispatched as they happen.
2. PayloadCreator
async (params, thunkApi) => {}
params
: What you’ll call the thunk with. This could be the request body, an id, queryString variables etcthunkApi
: An object containingdispatch
: To dispatch actions other than the three that are generatedgetState
: Gets the current root store stateextra
: The argument passed toconfigureStore({middleware: getDefaultMiddleware => getDefaultMiddleware({thunk: {extraArgument: "**this**"}})})
requestId
: unique generated IDsignal
: AbortController.signal when you want to implement cancellation (see Cancellation)rejectWithValue(value; [meta])
andfulfillWithValue(value, [meta])
: or just throw/return!
3. Options
An option object that can contain:
condition
: if this function returns false, no actions will be dispatched, see Canceling before executiondispatchConditionRejection
: ifcondition
returned false and this field is true, the rejected action will still be dispatched
idGenerator
: Uses nanoid by defaultserializeError
: Serialize errors in a different way than sindresorhus/serialize-errorgetPendingMeta
: merge extra data intopendingAction.meta
Unwrapping
createAsyncThunk
always returns a resolved promise, even if it was rejected.
To get the actual contents, .unwrap()
it!
dispatch(fetchStuff({id: 42}))
.unwrap()
.then(success => {})
.catch(error => {})
Matchers
We already saw the fetchStuff.settled
(for handling fulfilled or rejected) matcher above but there
are other ways
to handle multiple actions with a single matcher.
import { isPending } from "@reduxjs/toolkit";
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>
type PendingAction = ReturnType<GenericAsyncThunk['pending']>
type RejectedAction = ReturnType<GenericAsyncThunk['rejected']>
type FulfilledAction = ReturnType<GenericAsyncThunk['fulfilled']>
const mySlice = createSlice({
extraReducers: builder => {
builder.addMatcher<RejectedAction>(
action => action.type.endsWith('/rejected'),
(state, action) => {
state.status = 'failed';
}
);
builder.addMatcher(isPending, (state, action) => {
// Or by using the existing isPending matcher utility
// Other utilities: isAllOf, isAnyOf
// isAsyncThunkAction, isFulfilled, isRejected(WithValue)
});
builder.addDefaultCase((state, action) => {
// Handle everything that was not previously handled
// Contrast to addMatcher which may handle an action
// that was already handled by other case / matchers
});
}
});
Reducer Creator
This alternative reducers
creation allows you to add thunks
without having to revert to extraReducers
.
import { buildCreateSlice, asyncThunkCreator } from "@reduxjs/toolkit";
// Extra setup required for adding thunks to reducers
const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
});
const creatorSlice = createAppSlice({
initialState: [],
reducers: create => ({
deleteEntity: create.reducer<{id: number}>((state, action) => {
return state.filter(x => x.id !== action.payload);
}),
addPartial: create.preparedReducer(
(todo: Partial<Todo>) => {
return {
payload: {
text: 'Default Text',
...todo,
}
}
},
(state, action) => {
state.push(action.payload);
}
),
// See above, requires buildCreateSlice()
getCreatures: create.asyncThunk(
async (params, thunkApi): Promise<MyEntity> => {
const res = await fetch(`/api/resource/${params.id}`);
return res.json();
}, {
pending: state => {
console.log('Getting entity');
},
rejected: (state, action) => {
console.log('Failed getting entity');
},
fulfilled: (state, action) => {
state.push(action.payload);
}
}
),
}),
});