Photo by Lautaro Andreani on Unsplash
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.