DEV Community

Cover image for Authentication with React Form Wizard and Nodejs - Part 1
full-stack-concepts
full-stack-concepts

Posted on • Updated on

Authentication with React Form Wizard and Nodejs - Part 1

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

Image description

(4) Sign in form wizard with Context Store

Image description

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

Now install the following packages:

npm i --save cors express lowdb morgan nanoid
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can test the registration for any user from your browse with this GET route:

http://locahost:4000/register/user@example.com/mypassword
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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:

  1. UserSlice
  2. Store
  3. 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
Enter fullscreen mode Exit fullscreen mode

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;      
}
Enter fullscreen mode Exit fullscreen mode

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
      });
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

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
      });
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)