Authentication with React Form Wizard and Nodejs - Part 1
full-stack-concepts
Posted on March 12, 2022
Introduction
In Turn any Form into a stepper form wizard with UI, Hooks, Context, React-Hook-Form and Yup it was shown how you can improve user experience by breaking up extended forms into wizards with React, Material-UI and React-Hook-Forms. This tutorial aims to code a sign in and sign up stepper powered by a Nodejs back-end and uses the same architecture from the previous part with the exception of Redux which will be used to manage state at application level.
So what will be coded?
(1) Node.js back-end for registering and validating users
(2) Redux store for communicating with back-end
(3) Sign up form wizard with Context Store
(4) Sign in form wizard with Context Store
Prerequisites
In order to work with the concepts presented in this tutorial you should have a basic understanding of ES6, React hooks, functional components, Context, Redux and NodeJS. T
In this first part we'll set up the back-end server and the Redux Store. In the next part we'll code the form wizard steppers. The code for this project can be found on Github.
Let's start with installing a new React app.
npx create-react-app <your-app-name>
cd <your-app-name>
npm install
(1) NodeJS back-end
Let's code the server first. In your project directory create a folder called server. Now use your favorite package manager to init your local repo:
npm init
Now install the following packages:
npm i --save cors express lowdb morgan nanoid
Install nodemon.js globally, a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected. Install it globally with
npm i -g nodemon
You have installed lowdb, a tiny local JSON database for small NodeJS projects. This package stores data as an object and supports two operations: read and write. The server app will use express to register, read and update user objects/entries and nanoid to create user tokens.
Let's now create the node index file that will be served with nodemon.js.
index.js
import express from "express";
import cors from 'cors';
import morgan from "morgan";
import { Low, JSONFile } from 'lowdb';
import userRouter from './users.js';
import { nanoid } from 'nanoid';
const adapter = new JSONFile("db.json");
const db = new Low(adapter);
db.data = ({ users: [
{
id: 1,
role: 'admin',
email: 'admin@example.com' ,
password: '12345678',
firstName: 'Admin',
lastName: 'Adminstrator',
token: nanoid(30)
},
{
id: 2,
role: 'user',
email: 'johndoe@example.com',
password: '12345678',
firstName: 'John',
lastName: 'Doe',
token: nanoid(30)
}
]});
await db.write(db.data);
const PORT = process.env.PORT || 4000
const app = express();
app.db = db;
app.use(cors({origin: '*'}));
app.use(express.json());
app.use(morgan("dev"));
app.use("/users", userRouter);
const localRouter = express.Router();
localRouter.get("/", (req, res) => {
res.send('Only /users/* routes are supported ');
});
app.use(localRouter);
app.listen(PORT, () => console.log(`Listening on Port ${PORT}`));
This file initializes the database with two predefined user accounts and tells express to use routes from the users.js file. So let's add this file:
users.js
Your server is now ready to run on port 4000.
So let's start it with
npm start
You can test the registration for any user from your browse with this GET route:
http://locahost:4000/register/user@example.com/mypassword
(2) Redux store for communicating with back-end
Let's now move one dir up, to the root of directory and add the following packages to the React app:
npm i --save @hookform/resolvers @mui/icons-material
@mui/material @reduxjs/toolkit react-hook-form
react-hot-toast react-redux yup
Why would you implement Redux if React Context can do the job? That's is a matter of opinion. Redux has better code organization, great tools for debugging, designed for dynamic data and extendible as can be read in this article. Another great advantage is the usage of thunks or middleware that can be imported into other slices or parts of your store. But when you code a small project Redux is probably a form of overhead.
Let's now code the Redux store:
- UserSlice
- Store
- Wrap the App with Redux
Setting up the UserSlice
The UserSlice contains two functions that can be used with Redux's dispatch and getstate methods which will be called in the high order component of our form wizard. The state of these actions is managed in the extraReducers section. Actually it would be better to export these actions into a separate file so they can be called and used in other slices. Inside the src/ folder of your folder create a new folder named Store and code 'UserSlice.js'.
<your-app-name>/src/Store/UserSlice.js
Let's first crate a wrapper function for fetch requests and import relevant components.
/* eslint-disabled */
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const request = async (method, url, data) => {
let response = await fetch(
url,
{
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}
);
return response;
}
Now we need two middleware functions, one for registering new users and one for signing in. These functions are created with Redux createAsyncThunk so our app has access to the async request lifecycles rejected, pending and fulfilled which canbe used to manage the state of the application.
The login function:
export const loginUser = createAsyncThunk(
'user/login',
async ({ email, password }, thunkAPI) => {
try {
const url = 'http://localhost:4000/users/login';
const response = await request('POST', url, { email, password });
const data = await response.json();
if (response.status === 200) {
return {
...data,
status: true
};
} else {
return thunkAPI.rejectWithValue(data);
}
} catch (e) {
return thunkAPI.rejectWithValue({
status:false,
data: e.response.data
});
}
}
)
And the registration function:
export const signupUser = createAsyncThunk(
'user/signup',
async ({ email, password }, thunkAPI) => {
try {
const url = 'http://localhost:4000/users/register';
const response = await request('POST', url, { email, password });
let data = await response.json();
if (response.status === 200 || response.status === 201) {
return {
...data,
email: email,
status: data.status,
message: (data.message) ? data.message: null
}
} else {
return thunkAPI.rejectWithValue(data);
}
} catch (e) {
return thunkAPI.rejectWithValue({
status: false,
data: e.response.data
});
}
}
);
Let's now code the slice portion:
const initFetchState = {
fetching: false,
success: false,
error: false,
message: null
}
const initMemberState = {
token: null,
email: null
}
const initialState = {
loggedIn:false,
status: initFetchState,
member: initMemberState
};
const userSlice = createSlice({
name: 'user',
initialState: initialState,
reducers: {
clearState: state => { state = initialState; },
clearFetchStatus: state => {
state.status = initFetchState;
},
deleteUserToken: state => {
state.member = { ...state.member, token: null};
},
setuserToken: (state, action) => {
state.member = { ...state.member, token: action.payload };
},
logout: (state, action) => {
state = {
loggedn: false,
status: initFetchState,
member: initMemberState
};
},
},
extraReducers: {
[signupUser.fulfilled]: (state, { payload }) => {
state.status.fetching = false;
state.status.success = true;
state.member.email = payload.email;
return state;
},
[signupUser.pending]: (state) => {
state.status.fetching = true;
return state;
},
[signupUser.rejected]: (state, { payload }) => {
state.status.fetching= false;
state.status.error = true;
state.status.message = (payload) ? payload.message : 'Connection Error';
return state;
},
[loginUser.fulfilled]: (state, { payload }) => {
state.loggedIn = true;
state.member.token = payload.token;
state.member.email = payload.user.email;
state.member.id = payload.user.id;
state.status.fetching = false;
state.status.success = true;
return state;
},
[loginUser.rejected]: (state, { payload }) => {
state.status.fetching= false;
state.status.error = true;
state.status.message = (payload) ? payload.message : 'Connection Error';
return state;
},
[loginUser.pending]: (state) => {
state.status.fetching = true;
return state;
},
}
});
export const {
clearState,
setuserToken,
clearFetchStatus
} = userSlice.actions;
export default userSlice.reducer;
The Redux Store
Now set up the store that brings together the state, actions, and reducers that make up the app so state can be retrieved, updated and handle callbacks. Create the src/Store/index.js file:
import { combineReducers } from "redux";
import { configureStore } from "@reduxjs/toolkit";
import UserSlice from './UserSlice';
const rootReducer = combineReducers({
user: UserSlice
});
export const store = configureStore({
reducer: rootReducer,
});
Wrap the App with Redux
Finally 'wrap the app' with Redux by editing your src/index.js file:
The global store is now ready to be imported into our form stepper modules.
This tutorial continues in Authentication with React From Wizard and Nodejs - Part 2 which explains how to code the authication form wizards. The code for this project can be found on Github.
Posted on March 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.