Published:

React + Redux - HTTP POST Request in Async Action with createAsyncThunk

Tutorial built with React 18.1.0, Redux 4.2.0 and Redux Toolkit 1.8.2

This is a quick example of how to send an HTTP POST request to an API in Redux using an async action created with the Redux Toolkit's createAsyncThunk() function.

The below code snippets show how to POST login credentials from a form in a React component to an API using a Redux action, and execute different logic based on the result - success or failure. They're from a React + Redux JWT authentication tutorial I posted recently that includes a live demo, to see the complete code and running example check out React 18 + Redux - JWT Authentication Example & Tutorial.

 

Redux Auth Slice

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

The auth slice manages Redux state, actions and reducers for authentication. The file is organised into three sections to make it easier to see what's going on. The first section calls functions to create and configure the slice, the second section exports the actions and reducer, and the third section contains the functions that implement the logic.

initialState defines the state properties in the slice with their initial values. The user state property holds the current logged in user, it is initialized with the 'user' object from local storage to support staying logged in between page refreshes and browser sessions, or null if localStorage is empty. The error is displayed in the login component if login failed.

The reducers object passed to createSlice() contains logic for synchronous actions (things you don't have to wait for). For example the logout reducer sets the user state property to null, removes it from local storage and redirects to the login page. It doesn't perform any async tasks such as an API request. The createSlice() function auto generates matching actions for these reducers and exposes them via the slice.actions property.

Async Actions with createAsyncThunk()

The extraActions object contains logic for asynchronous actions (things you have to wait for) such as API requests. Async actions are created with the Redux Toolkit createAsyncThunk() function. The first parameter to createAsyncThunk is the name of the action, the standard convention for Redux action names is '[slice name]/[action name]' e.g. ('auth/login'). The second parameter is the async function that performs the action and returns the result when it's finished.

For each async action created with createAsyncThunk(), three Redux actions are automatically generated by the Redux Toolkit, one for each stage of the async action: pending, fulfilled, rejected.

The extraReducers object contains methods for updating Redux state at each of the three different stages of async actions generated by createAsyncThunk(), and the object passed as a parameter to the createSlice() function to include the extra reducers in the Redux slice.

HTTP POST from Login Action Method

The login() action method posts credentials to the API, on success (fulfilled) the returned user object is stored in the Redux state user prop and localStorage, and the user is redirected to the return url or home page. On fail (rejected) the error is stored in the Redux state error property which is rendered by the login component.

Export Actions and Reducer for Redux Slice

The authActions export includes all sync actions (slice.actions) and async actions (extraActions) for the auth slice.

The reducer for the auth slice is exported as authReducer, which is used in the Redux store for the app to configure the global state store.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { history, fetchWrapper } from '_helpers';

// create slice

const name = 'auth';
const initialState = createInitialState();
const reducers = createReducers();
const extraActions = createExtraActions();
const extraReducers = createExtraReducers();
const slice = createSlice({ name, initialState, reducers, extraReducers });

// 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
        user: JSON.parse(localStorage.getItem('user')),
        error: null
    }
}

function createReducers() {
    return {
        logout
    };

    function logout(state) {
        state.user = null;
        localStorage.removeItem('user');
        history.navigate('/login');
    }
}

function createExtraActions() {
    const baseUrl = `${process.env.REACT_APP_API_URL}/users`;

    return {
        login: login()
    };    

    function login() {
        return createAsyncThunk(
            `${name}/login`,
            async ({ username, password }) => await fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
        );
    }
}

function createExtraReducers() {
    return {
        ...login()
    };

    function login() {
        var { pending, fulfilled, rejected } = extraActions.login;
        return {
            [pending]: (state) => {
                state.error = null;
            },
            [fulfilled]: (state, action) => {
                const user = action.payload;
                
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                state.user = user;

                // get return url from location state or default to home page
                const { from } = history.location.state || { from: { pathname: '/' } };
                history.navigate(from);
            },
            [rejected]: (state, action) => {
                state.error = action.error;
            }
        };
    }
}
 

Redux Store

Path: /src/_store/index.js

The store index file configures the root Redux store for the React application with the configureStore() function. The returned Redux store contains the state properties auth and users which map to their corresponding slices.

The index file also re-exports all of the modules from the Redux slices in the folder. This enables Redux modules to be imported directly from the _store folder without the path to the slice file. It also enables multiple imports from different files at once (e.g. import { store, authActions } from '_store';)

import { configureStore } from '@reduxjs/toolkit';

import { authReducer } from './auth.slice';
import { usersReducer } from './users.slice';

export * from './auth.slice';
export * from './users.slice';

export const store = configureStore({
    reducer: {
        auth: authReducer,
        users: usersReducer
    },
});
 

React Login Component

Path: /src/login/Login.jsx

The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the React + Redux app.

Form validation rules are defined with the Yup schema validation library and passed with formOptions to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

HTTP POST Request in Redux from Login Form

The onSubmit function gets called when the form is submitted and valid, and sends user credentials to the API in an HTTP POST request by calling dispatch(authActions.login({ username, password })). The dispatch() function dispatches the async login action method to the Redux store.

On successful authentication the user data (including JWT token) is saved in Redux shared state by the login.fulfilled reducer in the auth slice, and the user is redirected to the home page.

On fail the error message is saved in Redux state by the login.rejected reducer. The error is automatically rendered as an alert at the bottom of the login form ({authError.message}).

The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('username')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useEffect } from 'react';
import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
import { useSelector, useDispatch } from 'react-redux';

import { history } from '_helpers';
import { authActions } from '_store';

export { Login };

function Login() {
    const dispatch = useDispatch();
    const authUser = useSelector(x => x.auth.user);
    const authError = useSelector(x => x.auth.error);

    useEffect(() => {
        // redirect to home if already logged in
        if (authUser) history.navigate('/');

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // form validation rules 
    const validationSchema = Yup.object().shape({
        username: Yup.string().required('Username is required'),
        password: Yup.string().required('Password is required')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

    // get functions to build form with useForm() hook
    const { register, handleSubmit, formState } = useForm(formOptions);
    const { errors, isSubmitting } = formState;

    function onSubmit({ username, password }) {
        return dispatch(authActions.login({ username, password }));
    }

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            <div className="alert alert-info">
                Username: test<br />
                Password: test
            </div>
            <div className="card">
                <h4 className="card-header">Login</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <label>Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={isSubmitting} className="btn btn-primary">
                            {isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                            Login
                        </button>
                        {authError &&
                            <div className="alert alert-danger mt-3 mb-0">{authError.message}</div>
                        }
                    </form>
                </div>
            </div>
        </div>
    )
}
 


Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter, Facebook or GitHub to be notified when I post new content.

Other than coding...

I'm currently attempting to travel around Australia by motorcycle with my wife Tina on a pair of Royal Enfield Himalayans. You can follow our adventures on YouTube, Instagram and Facebook.


Need Some React Help?

Search fiverr to find help quickly from experienced React developers.



Supported by