Published: March 16 2023

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()

Path: /src/_store/auth.slice.js

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

Path: /src/_store/alert.slice.js

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

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by