Modern React Redux Toolkit - Login & User Registration Tutorial and Example
Modern React Redux Toolkit - Login & User Registration Tutorial and Example
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