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
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
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
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>
)
}
Need Some React Help?
Search fiverr for freelance React 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!