Building Front-End Authentication Using Redux Toolkit and Axios

Introduction

Redux toolkit is a powerful react state management library useful for creating large applications and project, it consists of all the futures of redux along side added functionalities which makes it great for large scale applications.

In this tutorial, I’m going to walk you through using the awesome react state management tool, redux toolkit, to build a nice looking front-end user login and register authentication.

We will make use of the axios API library to completely fetch API data from any backend, alongside user login , register, configure createSlice() and thunkAPI() to manipulate user data.

Prerequisite

Ensure to have the following in other to ensure seamless study of this article:

  • Basic knowledge of HTML, CSS and JavaScript.
  • An IDE on your computer - I made use of Visual Studio Code for this tutorial

You can code along to include this authentication system to your project or clone my Github Repository. Do lookup the basics of redux toolkit on its official documentation Here

Redux Toolkit Installation

Go ahead and set up react installations with the following command.

npx create-react-app frontend --template redux

What the above command does is to install redux toolkit as well as various other packages and dependencies, redux toolkit takes much of the traditional redux boilerplate code away.

Redux toolkit folder structure is set, install router-dom and other usefull dependencies with the command

npm i react-router-dom react-toastify axios react-icons react-modal
  • react-router-dom: to manage our routes and link pages
  • react-toastify: a simple library to manage alerts like error or success
  • axios: a library for managing API
  • react-icons: a package for getting icons fully functional and can easily be customized
  • react-modal: a mockup for our data

ReactJS Registration and Login Form

Let’s start by building a basic Register and Login form on the source (src) folder of our React App. I included my own forms on my github folder alongside all code mockups for our front-end authentication app. Github

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { FaUser } from 'react-icons/fa'

function Register() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    password: '',
    password2: '',
  })

  const { name, email, password, password2 } = form

  const onChange = (e) => {
    setForm((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }))
  }


  const onSubmit = (e) => {
    e.preventDefault()

    if (password !== password2) {
      toast.error('Passwords Mismatch')
    } else {
      const userData = {
        name,
        email,
        password,
      }
    }
  }

  return (
    <>
      <section className='heading'>
        <h1>
          <FaUser /> Register
        </h1>
        <p>Please create an account</p>
      </section>

      <section className='form'>
        <form onSubmit={onSubmit}>
          <div className='form-group'>
            <input
              type='text'
              className='form-control'
              id='name'
              name='name'
              value={name}
              onChange={onChange}
              placeholder='Enter your name'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='email'
              className='form-control'
              id='email'
              name='email'
              value={email}
              onChange={onChange}
              placeholder='Enter your email'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password'
              name='password'
              value={password}
              onChange={onChange}
              placeholder='Enter password'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password2'
              name='password2'
              value={password2}
              onChange={onChange}
              placeholder='Confirm password'
              required
            />
          </div>
          <div className='form-group'>
            <button className='btn btn-block'>Submit</button>
          </div>
        </form>
      </section>
    </>
  )
}

export default Register

Create a Login page file and edit as so:

import { useState } from 'react'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
import { FaSignInAlt } from 'react-icons/fa'

function Login() {
  const [form, setForm] = useState({
    email: '',
    password: '',
  })

  const { email, password } = form

  const onChange = (e) => {
    setForm((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }))
  }

  const onSubmit = (e) => {
    e.preventDefault()

    const userData = {
      email,
      password,
    }
  }

  return (
    <>
      <section className='heading'>
        <h1>
          <FaSignInAlt /> Login
        </h1>
        <p>Please log in to get support</p>
      </section>

      <section className='form'>
        <form onSubmit={onSubmit}>
          <div className='form-group'>
            <input
              type='email'
              className='form-control'
              id='email'
              name='email'
              value={email}
              onChange={onChange}
              placeholder='Enter your email'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password'
              name='password'
              value={password}
              onChange={onChange}
              placeholder='Enter password'
              required
            />
          </div>
          <div className='form-group'>
            <button className='btn btn-block'>Submit</button>
          </div>
        </form>
      </section>
    </>
  )
}

export default Login

Redux Toolkit Usage Guide

Now we have to add our functionalities, redux toolkit comes out of the box with some useful functions :

  • configureStore() : it automatically combines reducers and any other middleware you provide
  • createAsyncThunk(): it returns a promise and works with async and await as standard practice to accept action type and upon given promise.
  • createAction(): for every given action type string, it generates an action creator
  • createReducer(): lets you write easy to read immutable codes and supply action types to case reducer functions.
  • createSlice(): It takes out a lot of all the boilerplate used on the traditional redux

Here we made use of the createSlice() and createAsyncThunk() to manipulate our state, extraReducers() allow us to work with cases from Async function in different lifecycle (fulfilled or pending).

One of the major properties of toolkit is that each slice reducer has its own slice of state and can be manipulated with the extraReducers()

Create an AuthSlice.js file and edit as so:

import { createSlice, createAsyncThunk, createAction } from '@reduxjs/toolkit'
import authService from './authService'
//  use a extractErrorMessage function to save some repetition
import { extractErrorMessage } from '../.. /utils'

// Get user from localstorage
const user = JSON.parse(localStorage.getItem('user'))


// There is no need for a reset function as we can do this in our pending cases
// No need for isError or message as we can catch the AsyncThunkAction rejection
const initialState = { 
  user: user ? user : null,
  isLoading: false, 
}

// Register new user
export const register = createAsyncThunk(
  'auth/register',
  async (user, thunkAPI) => {
    try {
      return await authService.register(user)
    } catch (error) {
      return thunkAPI.rejectWithValue(extractErrorMessage(error))
    }
  }
)

// Login user
export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => {
  try {
    return await authService.login(user)
  } catch (error) {
    return thunkAPI.rejectWithValue(extractErrorMessage(error))
  }
})

// Logout user

export const logout = createAction('auth/logout', () => {
  authService.logout()
  return {}
})

export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    logout: (state) => {
      state.user = null
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(register.pending, (state) => {
        state.isLoading = true
      })
      .addCase(register.fulfilled, (state, action) => {
        state.user = action.payload
        state.isLoading = false
      })
      .addCase(register.rejected, (state) => {
        state.isLoading = false
      })
      .addCase(login.pending, (state) => {
        state.isLoading = false
      })
      .addCase(login.fulfilled, (state, action) => {
        state.user = action.payload
        state.isLoading = false
      })
      .addCase(login.rejected, (state) => {
        state.isLoading = false
      })
  },
})

export default authSlice.reducer

The function extractErrorMessage() was created seperately so as to make our code more readable and easy to maintain and imported as utils.js

Create a utils.js file in your root folder and edit as so:

export function extractErrorMessage(error) {
  return error.response?.data?.message || error.message || error.toString()
}

The redux store.js component holds the entire tree of all the state in our application.

Configure the store.js using configureStore() as redux toolkit saves us from a lot of boilerplate as compared to the traditional redux, notice the authReducer is imported and hooked up to our redux store as so:

import { configureStore } from '@reduxjs/toolkit'
import authReducer from '../features/auth/authSlice'

export const store = configureStore({
  reducer: {
    auth: authReducer

  },
})

Importing the following methods from react-redux, useDispatch() and the useSelector(), useSelector() helps us selects data from react global states, usedispatch() hook will dispatch all our actions such as login, register.

Notice the useSelector(state) => state.auth, auth points to the global state we defined in the createslice() method. Using the createAsyncThunk() method we will hooked up the login and register function asynchronously and fetch data using axios on the authService.js file below.

import axios from 'axios'

const API_URL = '/api/users'

// Register user
const register = async (userData) => {
  const response = await axios.post(API_URL, userData)

  if (response.data) {
    localStorage.setItem('user', JSON.stringify(response.data))
  }
  return response.data
}

// Login user
const login = async (userData) => {
  const response = await axios.post(API_URL + '/login', userData)

  if (response.data) {
    localStorage.setItem('user', JSON.stringify(response.data))
  }
  return response.data
}

// Logout user
const logout = () => localStorage.removeItem('user')

const authService = {
  register,
  logout,
  login,
}

export default authService

Refactoring React User Registration and Login Form

createSlice() and authService.js file is ready to be hooked into the register and login form, go ahead edit both forms as so;

Inside the Register form, refactor as this

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { FaUser } from 'react-icons/fa'
import { useSelector, useDispatch } from 'react-redux'
import { register } from '../features/auth/authSlice'

function Register() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    password2: '',
  })

  const { name, email, password, password2 } = formData

  const dispatch = useDispatch()
  const navigate = useNavigate()

  const { isLoading } = useSelector((state) => state.auth)

  const onChange = (e) => {
    setFormData((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }))
  }


  const onSubmit = (e) => {
    e.preventDefault()

    if (password !== password2) {
      toast.error('Passwords do not match')
    } else {
      const userData = {
        name,
        email,
        password,
      }

      dispatch(register(userData))
        .unwrap()
        .then((user) => {
          toast.success(`Registered new user - ${user.name}`)
          navigate('/')
        })
        .catch(toast.error)
    }
  }

  if (isLoading) {
    return <h2>Loading...</h2>
  }

  return (
    <>
      <section className='heading'>
        <h1>
          <FaUser /> Register
        </h1>
        <p>Please create an account</p>
      </section>

      <section className='form'>
        <form onSubmit={onSubmit}>
          <div className='form-group'>
            <input
              type='text'
              className='form-control'
              id='name'
              name='name'
              value={name}
              onChange={onChange}
              placeholder='Enter your name'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='email'
              className='form-control'
              id='email'
              name='email'
              value={email}
              onChange={onChange}
              placeholder='Enter your email'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password'
              name='password'
              value={password}
              onChange={onChange}
              placeholder='Enter password'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password2'
              name='password2'
              value={password2}
              onChange={onChange}
              placeholder='Confirm password'
              required
            />
          </div>
          <div className='form-group'>
            <button className='btn btn-block'>Submit</button>
          </div>
        </form>
      </section>
    </>
  )
}

export default Register

Do same to the Login file

import { useState } from 'react'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
import { FaSignInAlt } from 'react-icons/fa'
import { useSelector, useDispatch } from 'react-redux'
import { login } from '../features/auth/authSlice'

function Login() {
  const [form, setForm] = useState({
    email: '',
    password: '',
  })

  const { email, password } = form

  const dispatch = useDispatch()
  const navigate = useNavigate()

  const { isLoading } = useSelector((state) => state.auth)

  const onChange = (e) => {
    setForm((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }))
  }

  const onSubmit = (e) => {
    e.preventDefault()

    const userData = {
      email,
      password,
    }

    dispatch(login(userData))
      .unwrap()
      .then((user) => {
        toast.success(`Logged in as ${user.name}`)
        navigate('/')
      })
      .catch(toast.error)
  }

  if (isLoading) {
    return <h2>Loading...</h2>
  }

  return (
    <>
      <section className='heading'>
        <h1>
          <FaSignInAlt /> Login
        </h1>
        <p>Please log in to get support</p>
      </section>

      <section className='form'>
        <form onSubmit={onSubmit}>
          <div className='form-group'>
            <input
              type='email'
              className='form-control'
              id='email'
              name='email'
              value={email}
              onChange={onChange}
              placeholder='Enter your email'
              required
            />
          </div>
          <div className='form-group'>
            <input
              type='password'
              className='form-control'
              id='password'
              name='password'
              value={password}
              onChange={onChange}
              placeholder='Enter password'
              required
            />
          </div>
          <div className='form-group'>
            <button className='btn btn-block'>Submit</button>
          </div>
        </form>
      </section>
    </>
  )
}

export default Login

From the above snippet, we made use of useSelect()to manipulate the authSlice.js file and get the global state of the user objects, and we used the useDispatch() to return a reference of the createSlice() function from the redux store

Conclusion

Now our application is up and running, we got to learn about some very key functions and methods that comes along with the redux toolkit.

Redux toolkit is more useful for creating large applications due to the power it has on managing multiple slices of state asynchronously. It can become an over kill when working with smaller applications.