User Authentication is one of the common workflow in web applications. In this tutorial, we will see how to build a User Login and Signup workflow with Modern react redux toolkit.
Demo
Let's scaffold an application using the command,
npx create-react-app redux-workflow --template redux
If you're completely new to redux-toolkit, checkout this article to learn the basic concepts of redux toolkit.
Let me give you a glimpse about the concepts of redux toolkit. Everything in toolkit is grouped as Features. it's called duck pattern.
Action and Reducers are combined in redux toolkit as Slice
. To make HTTP API call, we will be using createAsyncThunk
. We will discuss about it in detail in the later part of the article.
Create App.js
importReactfrom'react';import'./App.css';import{BrowserRouterasRouter,Switch,Route,Link}from'react-router-dom';importLoginfrom'./features/User/Login';importSignupfrom'./features/User/Signup';importDashboardfrom'./features/User/Dashboard';import{PrivateRoute}from'./helpers/PrivateRoute';functionApp(){return(<divclassName="App"><Router><Switch><Routeexactcomponent={Login}path="/login"/><Routeexactcomponent={Signup}path="/signup"/><PrivateRouteexactcomponent={Dashboard}path="/"/></Switch></Router></div>);}exportdefaultApp;
Before creating components for the workflow. let's create redux slice for our User section. create UserSlice.js
inside features/User
directory,
import{createSlice,createAsyncThunk}from'@reduxjs/toolkit';exportconstuserSlice=createSlice({name:'user',initialState:{username:'',email:'',isFetching:false,isSuccess:false,isError:false,errorMessage:'',},reducers:{// Reducer comes here},extraReducers:{// Extra reducer comes here},});exportconstuserSelector=(state)=>state.user;
Here, we use createSlice
which handles the action and reducer in a single function. After that, add the reducer in redux store
app/store.js
import{configureStore}from'@reduxjs/toolkit';import{userSlice}from'../features/User/UserSlice';exportdefaultconfigureStore({reducer:{user:userSlice.reducer,},});
Signup Functionality
Once we create a basic structure for redux and store. it's time to create components for the application. Create Signup.js
inside features/User
directory,
importReact,{Fragment,useEffect}from'react';import{Link}from'react-router-dom';import{useForm}from'react-hook-form';import{useSelector,useDispatch}from'react-redux';import{signupUser,userSelector,clearState}from'./UserSlice';import{useHistory}from'react-router-dom';importtoastfrom'react-hot-toast';constSignup=()=>{constdispatch=useDispatch();const{register,errors,handleSubmit}=useForm();consthistory=useHistory();const{isFetching,isSuccess,isError,errorMessage}=useSelector(userSelector);constonSubmit=(data)=>{dispatch(signupUser(data));};useEffect(()=>{return()=>{dispatch(clearState());};},[]);useEffect(()=>{if(isSuccess){dispatch(clearState());history.push('/');}if(isError){toast.error(errorMessage);dispatch(clearState());}},[isSuccess,isError]);return(<Fragment><divclassName="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"><divclass="sm:mx-auto sm:w-full sm:max-w-md"><h2class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign Up to your account
</h2></div><divclassName="mt-8 sm:mx-auto sm:w-full sm:max-w-md"><divclassName="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"><formclassName="space-y-6"onSubmit={handleSubmit(onSubmit)}method="POST">{*/ Form Comes Here */}</form><divclass="mt-6"><divclass="relative"><divclass="relative flex justify-center text-sm"><spanclass="px-2 bg-white text-gray-500">
Or <Linkto="login"> Login</Link></span></div></div></div></div></div></div></Fragment>);};exportdefaultSignup;
Here, we use React Hook Form to handle Form validation. Whenever we want to dispatch an action in redux, we use useDispatch
provided by react-redux
.
constdispatch=useDispatch();
We can access redux state in component using hooks, useSelector
const{isFetching,isSuccess,isError,errorMessage}=useSelector(userSelector);
Now, when an user submits a signup form, we need to dispatch an action by passing required data.
constonSubmit=(data)=>{dispatch(signupUser(data));};
Let's create that action in UserSlice.js
exportconstsignupUser=createAsyncThunk('users/signupUser',async({name,email,password},thunkAPI)=>{try{constresponse=awaitfetch('https://mock-user-auth-server.herokuapp.com/api/v1/users',{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json',},body:JSON.stringify({name,email,password,}),});letdata=awaitresponse.json();console.log('data',data);if(response.status===200){localStorage.setItem('token',data.token);return{...data,username:name,email:email};}else{returnthunkAPI.rejectWithValue(data);}}catch(e){console.log('Error',e.response.data);returnthunkAPI.rejectWithValue(e.response.data);}});
Main purpose of using createAsyncThunk
is it provides the API state out of the box. In traditional redux way, we need to handle the api state such as loading
, success
and failed
.
createAsyncThunk
provides us those states out of the box. To implement it, we just need to use the action name and the state of it.
createAsyncThunk
takes two argument,
- Name that helps to identify action types.
- A callback function that should return a
promise
Further, callback function take two arguments. first, is the value that we pass from dispatched action and second argument is Thunk API config.
Once it returns a promise, either it will resolve or reject the promise. By default it provides us three state which are pending
, fulfilled
and rejected
.
extraReducers:{[signupUser.fulfilled]:(state,{payload})=>{state.isFetching=false;state.isSuccess=true;state.email=payload.user.email;state.username=payload.user.name;},[signupUser.pending]:(state)=>{state.isFetching=true;},[signupUser.rejected]:(state,{payload})=>{state.isFetching=false;state.isError=true;state.errorMessage=payload.message;}}
It updates the redux state which will update our component using hook useSelector
. Once the signup successfully, it redirects to dashboard
component.
useEffect(()=>{if(isSuccess){dispatch(clearState());history.push('/');}if(isError){toast.error(errorMessage);dispatch(clearState());}},[isSuccess,isError]);
Login Functionality
Most of the logic will be similar to login workflow. create Login.js
inside features/User
directory and add the following code,
importReact,{Fragment,useEffect}from'react';import{Link}from'react-router-dom';import{useForm}from'react-hook-form';import{useSelector,useDispatch}from'react-redux';import{loginUser,userSelector,clearState}from'./UserSlice';importtoastfrom'react-hot-toast';import{useHistory}from'react-router-dom';constLogin=({})=>{constdispatch=useDispatch();consthistory=useHistory();const{register,errors,handleSubmit}=useForm();const{isFetching,isSuccess,isError,errorMessage}=useSelector(userSelector);constonSubmit=(data)=>{dispatch(loginUser(data));};useEffect(()=>{return()=>{dispatch(clearState());};},[]);useEffect(()=>{if(isError){toast.error(errorMessage);dispatch(clearState());}if(isSuccess){dispatch(clearState());history.push('/');}},[isError,isSuccess]);return(<Fragment><divclassName="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"><divclass="sm:mx-auto sm:w-full sm:max-w-md"><h2class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2></div><divclassName="mt-8 sm:mx-auto sm:w-full sm:max-w-md"><divclassName="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"><formclassName="space-y-6"onSubmit={handleSubmit(onSubmit)}method="POST">{*/ Login Form Comes Here */}</form><divclass="mt-6"><divclass="relative"><divclass="relative flex justify-center text-sm"><spanclass="px-2 bg-white text-gray-500">
Or <Linkto="signup"> Signup</Link></span></div></div></div></div></div></div></Fragment>);};exportdefaultLogin;
Here, we dispatch loginUser
action which makes HTTP call in the redux slice.
constonSubmit=(data)=>{dispatch(loginUser(data));};
create an AsyncThunk
function inside UserSlice.js
and add the following code,
exportconstloginUser=createAsyncThunk('users/login',async({email,password},thunkAPI)=>{try{constresponse=awaitfetch('https://mock-user-auth-server.herokuapp.com/api/v1/auth',{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json',},body:JSON.stringify({email,password,}),});letdata=awaitresponse.json();console.log('response',data);if(response.status===200){localStorage.setItem('token',data.token);returndata;}else{returnthunkAPI.rejectWithValue(data);}}catch(e){console.log('Error',e.response.data);thunkAPI.rejectWithValue(e.response.data);}});
Promise will either be resolved or rejected based on HTTP call, let's handle it inside our reducer with the states,
[loginUser.fulfilled]:(state,{payload})=>{state.email=payload.email;state.username=payload.name;state.isFetching=false;state.isSuccess=true;returnstate;},[loginUser.rejected]:(state,{payload})=>{console.log('payload',payload);state.isFetching=false;state.isError=true;state.errorMessage=payload.message;},[loginUser.pending]:(state)=>{state.isFetching=true;},
Once it updates our redux state, we will use it inside our component to render the result.
const{isFetching,isSuccess,isError,errorMessage}=useSelector(userSelector);// Update UI based on the redux state(Success or Error)useEffect(()=>{if(isError){toast.error(errorMessage);dispatch(clearState());}if(isSuccess){dispatch(clearState());history.push('/');}},[isError,isSuccess]);
Finally our Dashboard.js
will be rendered with update user state from redux,
importReact,{Fragment,useEffect}from'react';import{useSelector,useDispatch}from'react-redux';import{userSelector,fetchUserBytoken,clearState}from'./UserSlice';importLoaderfrom'react-loader-spinner';import{useHistory}from'react-router-dom';constDashboard=()=>{consthistory=useHistory();constdispatch=useDispatch();const{isFetching,isError}=useSelector(userSelector);useEffect(()=>{dispatch(fetchUserBytoken({token:localStorage.getItem('token')}));},[]);const{username,email}=useSelector(userSelector);useEffect(()=>{if(isError){dispatch(clearState());history.push('/login');}},[isError]);constonLogOut=()=>{localStorage.removeItem('token');history.push('/login');};return(<divclassName="container mx-auto">{isFetching?(<Loadertype="Puff"color="#00BFFF"height={100}width={100}/>):(<Fragment><divclassName="container mx-auto">
Welcome back <h3>{username}</h3></div><buttononClick={onLogOut}className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
Log Out
</button></Fragment>)}</div>);};exportdefaultDashboard;
Complete source code is available here