Redux Toolkit - Thunk
- What is a Thunk?
-
Handling asynchronous actions with
createAsyncThunk
- Creating a Thunk
- Basic Example
- More Resources
What is a Thunk?
Beside being a dull, heavy sound, such as that made by an object falling to the ground, 'thunk' is a term used in programming to describe a function that wraps an expression to delay its evaluation.
In the context of Redux, a thunk is a function that can be dispatched to the store that can contain side effects, asynchronous requests, and other logic.
createAsyncThunk
Handling asynchronous actions with Asynchronous actions involve side effects like API requests, requiring you to wait for a response before updating the state. This typically happens when interacting with external systems like servers, where you need the response before updating the state.
Redux Toolkit offers createAsyncThunk
to define a thunk that dispatches pending
,
fulfilled
, and rejected
actions based on the returned promise's lifecycle. This
function simplifies the process by automatically dispatching the appropriate actions
based on the promise's status.
The createAsyncThunk
function takes two arguments:
- A string representing the action type.
- A function that returns a promise, known as the "payload creator".
export const myThunk = createAsyncThunk(
'feature/someAction', // name of the action
async () => { } // payload creator
);
Payload Creator
The payload creator is a function passed as the second argument to createAsyncThunk
.
It's responsible for performing the actual asynchronous operation, typically involving
an API request or any logic that requires waiting for a response.
This function returns a promise that resolves with the data obtained from the asynchronous operation or rejects with an error if something goes wrong.
The payload creator function can accept two arguments:
-
value
: The value passed to the action creator when it's dispatched. -
thunkAPI
: An object containing useful properties and functions for handling asynchronous logic.
export const someAction = createAsyncThunk(
'feature/someAction',
async (value, thunkAPI) => {
// Perform an asynchronous operation here
return // the result as a promise
}
);
Note: that the both the first and second arguments are optional. You only need to include
them if you need to access the value
or thunkAPI
values.
Creating a Thunk
Steps (high-level overview)
-
Create a slice
- Use
createSlice
from@reduxjs/toolki
t to define a Redux slice. - Define the
name
,initialState
,reducers
, andextraReducers
for the slice.
- Use
-
Define an async thunk using
createAsyncThunk
to handle asynchronous logic.- Add the action type string (e.g.,
feature/someAction
). - Add "payload creator" function(s), which are an async function that returns a promise.
- Add the action type string (e.g.,
-
Add Extra Reducers
- Inside your slice definition (
createSlice
), add extra reducers to handlepending
,fulfilled
, andrejected
actions dispatched by the async thunk.
- Inside your slice definition (
- Export the async thunk, action creators, and the slice reducer.
-
Dispatch Thunk
- In your React component, use the
useDispatch
hook fromreact-redux
to dispatch the async thunk.
- In your React component, use the
-
Handle State Changes
- Use the
useSelector
hook fromreact-redux
to select and react to state changes triggered by the async thunk.
- Use the
Basic Example
Things to remember
- When working with asynchronous functions, you always define the action first using
createAsyncThunk
, and then you define the reducers. - The way you define a reducer for an asynchronous function is different because it
doesn’t go in the
reducers
object. It actually goes at the bottom in something that we callextraReducers
. - You don’t need to worry too much about the
builder
, it’s just a tool that allows you to essentially add cases to the reducers. - Through the
builder
object, you can define how the state should be updated based on the different states of the promise (pending, fulfilled, rejected). - The naming of actions and reducers is automatically generated by Redux Toolkit. You only need to define the action name for asynchronous functions.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
const initialState = {
data: { count: 0 },
isLoading: false,
error: null
};
export const incrementByValueAsync = createAsyncThunk(
'counter/incrementByValueAsync',
async (amount) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return amount;
}
);
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: { },
extraReducers: (builder) => {
builder
.addCase(incrementByValueAsync.pending, () => {
console.log('incrementByValueAsync.pending');
})
.addCase(incrementByValueAsync.fulfilled, (state, action) => {
state.data.count += action.payload;
})
.addCase(incrementByValueAsync.rejected, () => {
console.error('incrementByValueAsync.rejected');
});
}
});
// export const {} = counterSlice.actions;
export default counterSlice.reducer;
Why do we use extraReducers
for asynchronous functions?
extraReducers
for asynchronous functions?extraReducers
object allows you to
define reducers for these states separately from the synchronous reducers. This makes it
easier to manage the different states and update the state accordingly.
extraReducers
works similarly to the reducers
object. However it handles asynchronous actions using the builder
object.