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
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Login from './features/User/Login';
import Signup from './features/User/Signup';
import Dashboard from './features/User/Dashboard';
import { PrivateRoute } from './helpers/PrivateRoute';
function App() {
return (
<div className="App">
<Router>
<Switch>
<Route exact component={Login} path="/login" />
<Route exact component={Signup} path="/signup" />
<PrivateRoute exact component={Dashboard} path="/" />
</Switch>
</Router>
</div>
);
}
export default App;
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';
export const userSlice = createSlice({
name: 'user',
initialState: {
username: '',
email: '',
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: '',
},
reducers: {
// Reducer comes here
},
extraReducers: {
// Extra reducer comes here
},
});
export 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
import { configureStore } from '@reduxjs/toolkit';
import { userSlice } from '../features/User/UserSlice';
export default configureStore({
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,
import React, { 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';
import toast from 'react-hot-toast';
const Signup = () => {
const dispatch = useDispatch();
const { register, errors, handleSubmit } = useForm();
const history = useHistory();
const { isFetching, isSuccess, isError, errorMessage } = useSelector(
userSelector
);
const onSubmit = (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>
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign Up to your account
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={handleSubmit(onSubmit)}
method="POST"
>
{*/ Form Comes Here */}
</form>
<div class="mt-6">
<div class="relative">
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">
Or <Link to="login"> Login</Link>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
};
export 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
.
const dispatch = 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.
const onSubmit = (data) => {
dispatch(signupUser(data));
};
Let's create that action in UserSlice.js
export const signupUser = createAsyncThunk(
'users/signupUser',
async ({ name, email, password }, thunkAPI) => {
try {
const response = await fetch(
'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,
}),
}
);
let data = await response.json();
console.log('data', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return { ...data, username: name, email: email };
} else {
return thunkAPI.rejectWithValue(data);
}
} catch (e) {
console.log('Error', e.response.data);
return thunkAPI.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,
import React, { 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';
import toast from 'react-hot-toast';
import { useHistory } from 'react-router-dom';
const Login = ({}) => {
const dispatch = useDispatch();
const history = useHistory();
const { register, errors, handleSubmit } = useForm();
const { isFetching, isSuccess, isError, errorMessage } = useSelector(
userSelector
);
const onSubmit = (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>
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={handleSubmit(onSubmit)}
method="POST"
>
{*/ Login Form Comes Here */}
</form>
<div class="mt-6">
<div class="relative">
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">
Or <Link to="signup"> Signup</Link>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
};
export default Login;
Here, we dispatch loginUser
action which makes HTTP call in the redux slice.
const onSubmit = (data) => {
dispatch(loginUser(data));
};
create an AsyncThunk
function inside UserSlice.js
and add the following code,
export const loginUser = createAsyncThunk(
'users/login',
async ({ email, password }, thunkAPI) => {
try {
const response = await fetch(
'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,
}),
}
);
let data = await response.json();
console.log('response', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return data;
} else {
return thunkAPI.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;
return state;
},
[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,
import React, { Fragment, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { userSelector, fetchUserBytoken, clearState } from './UserSlice';
import Loader from 'react-loader-spinner';
import { useHistory } from 'react-router-dom';
const Dashboard = () => {
const history = useHistory();
const dispatch = 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]);
const onLogOut = () => {
localStorage.removeItem('token');
history.push('/login');
};
return (
<div className="container mx-auto">
{isFetching ? (
<Loader type="Puff" color="#00BFFF" height={100} width={100} />
) : (
<Fragment>
<div className="container mx-auto">
Welcome back <h3>{username}</h3>
</div>
<button
onClick={onLogOut}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Log Out
</button>
</Fragment>
)}
</div>
);
};
export default Dashboard;
Complete source code is available here