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.
Let's scaffold an application using the command,
1npx 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
1import React from "react"2import "./App.css"3import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"4import Login from "./features/User/Login"5import Signup from "./features/User/Signup"6import Dashboard from "./features/User/Dashboard"7import { PrivateRoute } from "./helpers/PrivateRoute"89function App() {10 return (11 <div className="App">12 <Router>13 <Switch>14 <Route exact component={Login} path="/login" />15 <Route exact component={Signup} path="/signup" />16 <PrivateRoute exact component={Dashboard} path="/" />17 </Switch>18 </Router>19 </div>20 )21}2223export default App
Before creating components for the workflow. let's create redux slice for our User section. create UserSlice.js
inside features/User
directory,
1import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"23export const userSlice = createSlice({4 name: "user",5 initialState: {6 username: "",7 email: "",8 isFetching: false,9 isSuccess: false,10 isError: false,11 errorMessage: "",12 },13 reducers: {14 // Reducer comes here15 },16 extraReducers: {17 // Extra reducer comes here18 },19})2021export const userSelector = 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
1import { configureStore } from "@reduxjs/toolkit"2import { userSlice } from "../features/User/UserSlice"3export default configureStore({4 reducer: {5 user: userSlice.reducer,6 },7})
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,
1import React, { Fragment, useEffect } from 'react';2import { Link } from 'react-router-dom';3import { useForm } from 'react-hook-form';4import { useSelector, useDispatch } from 'react-redux';5import { signupUser, userSelector, clearState } from './UserSlice';6import { useHistory } from 'react-router-dom';7import toast from 'react-hot-toast';89const Signup = () => {10 const dispatch = useDispatch();11 const { register, errors, handleSubmit } = useForm();12 const history = useHistory();1314 const { isFetching, isSuccess, isError, errorMessage } = useSelector(15 userSelector16 );17 const onSubmit = (data) => {18 dispatch(signupUser(data));19 };2021 useEffect(() => {22 return () => {23 dispatch(clearState());24 };25 }, []);2627 useEffect(() => {28 if (isSuccess) {29 dispatch(clearState());30 history.push('/');31 }3233 if (isError) {34 toast.error(errorMessage);35 dispatch(clearState());36 }37 }, [isSuccess, isError]);3839 return (40 <Fragment>41 <div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">42 <div class="sm:mx-auto sm:w-full sm:max-w-md">43 <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">44 Sign Up to your account45 </h2>46 </div>47 <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">48 <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">49 <form50 className="space-y-6"51 onSubmit={handleSubmit(onSubmit)}52 method="POST"53 >54 {*/ Form Comes Here */}55 </form>56 <div class="mt-6">57 <div class="relative">58 <div class="relative flex justify-center text-sm">59 <span class="px-2 bg-white text-gray-500">60 Or <Link to="login"> Login</Link>61 </span>62 </div>63 </div>64 </div>65 </div>66 </div>67 </div>68 </Fragment>69 );70};7172export default Signup;
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
.
1const dispatch = useDispatch()
We can access redux state in component using hooks, useSelector
1const { isFetching, isSuccess, isError, errorMessage } = useSelector(2 userSelector3)
Now, when an user submits a signup form, we need to dispatch an action by passing required data.
1const onSubmit = data => {2 dispatch(signupUser(data))3}
Let's create that action in UserSlice.js
1export const signupUser = createAsyncThunk(2 "users/signupUser",3 async ({ name, email, password }, thunkAPI) => {4 try {5 const response = await fetch(6 "https://mock-user-auth-server.herokuapp.com/api/v1/users",7 {8 method: "POST",9 headers: {10 Accept: "application/json",11 "Content-Type": "application/json",12 },13 body: JSON.stringify({14 name,15 email,16 password,17 }),18 }19 )20 let data = await response.json()21 console.log("data", data)2223 if (response.status === 200) {24 localStorage.setItem("token", data.token)25 return { ...data, username: name, email: email }26 } else {27 return thunkAPI.rejectWithValue(data)28 }29 } catch (e) {30 console.log("Error", e.response.data)31 return thunkAPI.rejectWithValue(e.response.data)32 }33 }34)
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,
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
.
1extraReducers: {2[signupUser.fulfilled]: (state, { payload }) => {3 state.isFetching = false;4 state.isSuccess = true;5 state.email = payload.user.email;6 state.username = payload.user.name;7 },8 [signupUser.pending]: (state) => {9 state.isFetching = true;10 },11 [signupUser.rejected]: (state, { payload }) => {12 state.isFetching = false;13 state.isError = true;14 state.errorMessage = payload.message;15 }16}
It updates the redux state which will update our component using hook useSelector
. Once the signup successfully, it redirects to dashboard
component.
1useEffect(() => {2 if (isSuccess) {3 dispatch(clearState())4 history.push("/")5 }67 if (isError) {8 toast.error(errorMessage)9 dispatch(clearState())10 }11}, [isSuccess, isError])
A Hands-On Guidebook to Learn Cloud Native Web Development - From Zero To Production
Most of the logic will be similar to login workflow. create Login.js
inside features/User
directory and add the following code,
1import React, { Fragment, useEffect } from 'react';2import { Link } from 'react-router-dom';3import { useForm } from 'react-hook-form';4import { useSelector, useDispatch } from 'react-redux';5import { loginUser, userSelector, clearState } from './UserSlice';6import toast from 'react-hot-toast';7import { useHistory } from 'react-router-dom';89const Login = ({}) => {10 const dispatch = useDispatch();11 const history = useHistory();12 const { register, errors, handleSubmit } = useForm();13 const { isFetching, isSuccess, isError, errorMessage } = useSelector(14 userSelector15 );16 const onSubmit = (data) => {17 dispatch(loginUser(data));18 };1920 useEffect(() => {21 return () => {22 dispatch(clearState());23 };24 }, []);2526 useEffect(() => {27 if (isError) {28 toast.error(errorMessage);29 dispatch(clearState());30 }3132 if (isSuccess) {33 dispatch(clearState());34 history.push('/');35 }36 }, [isError, isSuccess]);3738 return (39 <Fragment>40 <div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">41 <div class="sm:mx-auto sm:w-full sm:max-w-md">42 <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">43 Sign in to your account44 </h2>45 </div>46 <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">47 <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">48 <form49 className="space-y-6"50 onSubmit={handleSubmit(onSubmit)}51 method="POST"52 >53 {*/ Login Form Comes Here */}54 </form>55 <div class="mt-6">56 <div class="relative">57 <div class="relative flex justify-center text-sm">58 <span class="px-2 bg-white text-gray-500">59 Or <Link to="signup"> Signup</Link>60 </span>61 </div>62 </div>63 </div>64 </div>65 </div>66 </div>67 </Fragment>68 );69};7071export default Login;
Here, we dispatch loginUser
action which makes HTTP call in the redux slice.
1const onSubmit = data => {2 dispatch(loginUser(data))3}
create an AsyncThunk
function inside UserSlice.js
and add the following code,
1export const loginUser = createAsyncThunk(2 "users/login",3 async ({ email, password }, thunkAPI) => {4 try {5 const response = await fetch(6 "https://mock-user-auth-server.herokuapp.com/api/v1/auth",7 {8 method: "POST",9 headers: {10 Accept: "application/json",11 "Content-Type": "application/json",12 },13 body: JSON.stringify({14 email,15 password,16 }),17 }18 )19 let data = await response.json()20 console.log("response", data)21 if (response.status === 200) {22 localStorage.setItem("token", data.token)23 return data24 } else {25 return thunkAPI.rejectWithValue(data)26 }27 } catch (e) {28 console.log("Error", e.response.data)29 thunkAPI.rejectWithValue(e.response.data)30 }31 }32)
Promise will either be resolved or rejected based on HTTP call, let's handle it inside our reducer with the states,
1[loginUser.fulfilled]: (state, { payload }) => {2 state.email = payload.email;3 state.username = payload.name;4 state.isFetching = false;5 state.isSuccess = true;6 return state;7 },8 [loginUser.rejected]: (state, { payload }) => {9 console.log('payload', payload);10 state.isFetching = false;11 state.isError = true;12 state.errorMessage = payload.message;13 },14 [loginUser.pending]: (state) => {15 state.isFetching = true;16 },
Once it updates our redux state, we will use it inside our component to render the result.
1const { isFetching, isSuccess, isError, errorMessage } = useSelector(2 userSelector3)45// Update UI based on the redux state(Success or Error)6useEffect(() => {7 if (isError) {8 toast.error(errorMessage)9 dispatch(clearState())10 }1112 if (isSuccess) {13 dispatch(clearState())14 history.push("/")15 }16}, [isError, isSuccess])
Finally our Dashboard.js
will be rendered with update user state from redux,
1import React, { Fragment, useEffect } from "react"2import { useSelector, useDispatch } from "react-redux"3import { userSelector, fetchUserBytoken, clearState } from "./UserSlice"4import Loader from "react-loader-spinner"5import { useHistory } from "react-router-dom"67const Dashboard = () => {8 const history = useHistory()910 const dispatch = useDispatch()11 const { isFetching, isError } = useSelector(userSelector)12 useEffect(() => {13 dispatch(fetchUserBytoken({ token: localStorage.getItem("token") }))14 }, [])1516 const { username, email } = useSelector(userSelector)1718 useEffect(() => {19 if (isError) {20 dispatch(clearState())21 history.push("/login")22 }23 }, [isError])2425 const onLogOut = () => {26 localStorage.removeItem("token")2728 history.push("/login")29 }3031 return (32 <div className="container mx-auto">33 {isFetching ? (34 <Loader type="Puff" color="#00BFFF" height={100} width={100} />35 ) : (36 <Fragment>37 <div className="container mx-auto">38 Welcome back <h3>{username}</h3>39 </div>4041 <button42 onClick={onLogOut}43 className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"44 >45 Log Out46 </button>47 </Fragment>48 )}49 </div>50 )51}5253export default Dashboard
Complete source code is available here
No spam, ever. Unsubscribe anytime.