Redux Toolkit createAsyncThunk - Dispatch a Redux Action from an Async Thunk in React with RTK
Tutorial built with React 18.2.0, Redux 4.2.1 and Redux Toolkit 1.9.3
This is a quick post to show how to dispatch a new Redux action inside an async thunk created with Redux Toolkit's createAsyncThunk()
function.
The example code is from of a React + Redux login tutorial I posted recently, the full project and documentation is available at React 18 + Redux - User Registration and Login Example & Tutorial.
A quick overview of Redux state management with Redux Toolkit (RTK)
Redux is a state management library for managing global state in a React application. The Redux Toolkit was created to simplify working with Redux and reduce the amount of boilerplate code required.
State and business logic are defined in Redux using a centralized store. With the Redux Toolkit a store is made up of one or more slices, each slice manages a section of state in the store. State is updated in Redux with actions and reducers, when an action is dispatched the Redux store executes a corresponding reducer function to update the state. Reducers cannot be called directly, they are called by Redux as the result of an action being dispatched.
Out of the box Redux only supports synchronous actions and reducers that update Redux state and don't perform any side effects. A side effect is anything other than updating the Redux state for the specified action.
Async actions and side effects with createAsyncThunk()
The Redux Toolkit (RTK) includes the createAsyncThunk()
function for creating a Redux Thunk. A thunk is a piece of Redux middleware for performing asynchronous actions such as API requests and side effects like redirects, accessing local storage and dispatching other Redux actions.
How to dispatch a Redux action from inside a thunk
The createAsyncThunk()
function accepts a payloadCreator
callback function as its second parameter, this is where the magic happens.
The payloadCreator
callback accepts two parameters, the first is the arg
passed to the action creator when it is dispatched (e.g. dispatch(authActions.login({ username, password }));
), the second is a thunkAPI
object that includes the Redux dispatch()
method that allows us to dispatch any other Redux action to our store.
Example Redux slice that dispatches actions from createAsyncThunk()
The auth slice manages Redux state, actions and reducers for authentication in the example React + Redux login app. The file is organised into three sections to make it easier to see what's going on. The first section (// create slice
) calls functions to create and configure the slice, the second section (// exports
) exports the actions and reducer, and the third section (// implementation
) contains the functions that implement the logic.
initialState
defines the state properties in the slice with their initial values.
reducers
contain logic to update state for synchronous actions (setAuth()
). The Redux Toolkit createSlice()
function auto generates matching actions for these reducers and exposes them via the slice.actions
property.
extraActions
contain Redux Thunks created with the RTK createAsyncThunk()
function.
Dispatch actions from createAsyncThunk()
The login()
action method dispatches a few different Redux actions. It calls dispatch(alertActions.clear());
to clear any alerts before sending a POST request to the API with user credentials. On success it calls dispatch(authActions.setAuth(user));
to store the returned user object in Redux state and login. On fail it calls dispatch(alertActions.error(error));
to display an error alert notification. Alert actions are defined in the alert slice.
The logout()
action method calls dispatch(authActions.setAuth(null));
to reset Redux auth state before clearing local storage and redirecting to the login page.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { alertActions } from '_store';
import { history, fetchWrapper } from '_helpers';
// create slice
const name = 'auth';
const initialState = createInitialState();
const reducers = createReducers();
const extraActions = createExtraActions();
const slice = createSlice({ name, initialState, reducers });
// exports
export const authActions = { ...slice.actions, ...extraActions };
export const authReducer = slice.reducer;
// implementation
function createInitialState() {
return {
// initialize state from local storage to enable user to stay logged in
value: JSON.parse(localStorage.getItem('auth'))
}
}
function createReducers() {
return {
setAuth
};
function setAuth(state, action) {
state.value = action.payload;
}
}
function createExtraActions() {
const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
return {
login: login(),
logout: logout()
};
function login() {
return createAsyncThunk(
`${name}/login`,
async function ({ username, password }, { dispatch }) {
dispatch(alertActions.clear());
try {
const user = await fetchWrapper.post(`${baseUrl}/authenticate`, { username, password });
// set auth user in redux state
dispatch(authActions.setAuth(user));
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('auth', JSON.stringify(user));
// get return url from location state or default to home page
const { from } = history.location.state || { from: { pathname: '/' } };
history.navigate(from);
} catch (error) {
dispatch(alertActions.error(error));
}
}
);
}
function logout() {
return createAsyncThunk(
`${name}/logout`,
function (arg, { dispatch }) {
dispatch(authActions.setAuth(null));
localStorage.removeItem('auth');
history.navigate('/account/login');
}
);
}
}
Redux alert slice
The alert slice manages Redux state, actions and reducers for alert notifications. It includes the alertActions.clear()
and alertActions.error()
actions that are dispatched from the auth slice.
initialState
defines the state properties for the 'alert'
slice with initial values. The value
state property holds the current alert notification, when it contains an object the alert is rendered at the top of the screen by the Alert
component, when the value is null
no alert is displayed.
reducers
contain logic to update state for different actions (success(), error(), clear()
). The Redux Toolkit createSlice()
function auto generates matching actions for these reducers and exposes them via the slice.actions
property.
Export actions and reducer for Redux slice
The alertActions
export includes all actions (slice.actions
) for the alert slice.
The reducer for the alert slice is exported as alertReducer
, which is used in the example React app to configure the global Redux store.
import { createSlice } from '@reduxjs/toolkit';
// create slice
const name = 'alert';
const initialState = createInitialState();
const reducers = createReducers();
const slice = createSlice({ name, initialState, reducers });
// exports
export const alertActions = { ...slice.actions };
export const alertReducer = slice.reducer;
// implementation
function createInitialState() {
return {
value: null
}
}
function createReducers() {
return {
success,
error,
clear
};
// payload can be a string message ('alert message') or
// an object ({ message: 'alert message', showAfterRedirect: true })
function success(state, action) {
state.value = {
type: 'alert-success',
message: action.payload?.message || action.payload,
showAfterRedirect: action.payload?.showAfterRedirect
};
}
function error(state, action) {
state.value = {
type: 'alert-danger',
message: action.payload?.message || action.payload,
showAfterRedirect: action.payload?.showAfterRedirect
};
}
function clear(state) {
// if showAfterRedirect flag is true the alert is not cleared
// for one route change (e.g. after successful registration)
if (state.value?.showAfterRedirect) {
state.value.showAfterRedirect = false;
} else {
state.value = null;
}
}
}
For more info on Redux see https://redux.js.org. For more info on slices and the Redux Toolkit see https://redux-toolkit.js.org.
Need Some Redux Help?
Search fiverr for freelance Redux developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!