Getting Started With MERN Stack (Part III)

Prakash
by Prakash 

MERN stack is a free JavaScript software stack for building dynamic web applications, consisting of four key technologies.

  • MongoDB
  • ExpressJS
  • ReactJS
  • NodeJS

This blog is the third part of a ‘Getting started with MERN stack’ series that aims to get you acquainted with the MERN stack. Read the first one here, and second one here before moving on with this one.

Setting up Create React Application

Make sure you’re in the root of the app and then run the following command:

npx create-react-app@latest frontend --template redux

The above command will install redux toolkit and react redux package.

Before using any built-in command, we need to go inside the project folder.

cd frontend

To run the app in development mode, you can use any of the below commands, and you will see the following message in your terminal

If you’re using yarn:

yarn start

Or, if you’re using npm

npm start

Now you’ll see the following message on your terminal

You can now view reactjs-boilerplate in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.254.6:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

Frontend tasks and features

We will work on three different features

  • Register new user
  • Authenticate a user
  • Logout

Dependencies Packages Installation

npm i react-icons axios react-toastify react-router-dom

We’ll call our APIs through axios

Creating the components

Inside the src folder(frontend), create another folder called pages, and inside it, create two different files Login.js Register.js We’ll work on these files later.

Setting up Route

We define all the routes, and for a specific path definition, its corresponding component will be rendered.

Cleaning up and updating the css

Prefer using 7-1 sass architecture for managing complex scss folders and files. For the compilation, we can use node-sass, which automatically compiles scss to css.

npm i node-sass

Adding feature components

Inside the features folder(frontend), create another folder called auth, and inside it, create two different files: authSlice.js and authService.js.

In the below code, the state is going to be updated according to the action triggered.

import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import authService from './authService'

// Get user from localStorage
const user = JSON.parse(localStorage.getItem('user'))

const initialState = {
 user: user ? user : null,
 isError: false,
 isSuccess: false,
 isLoading: false,
 message: ''
}

// Register user
export const register = createAsyncThunk('auth/register', async(user, thunkAPI) => {
 try{
   return await authService.register(user)
 } catch (error) {
   const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
   return thunkAPI.rejectWithValue(message)
 }
})

// Login user
export const login = createAsyncThunk('auth/login', async(user, thunkAPI) => {
 try{
   return await authService.login(user)
 } catch (error) {
   const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
   return thunkAPI.rejectWithValue(message)
 }
})
export const logout = createAsyncThunk('auth/logout', async () => {
 await authService.logout()
})

export const authSlice = createSlice({
 name: 'auth',
 initialState,
 reducers: {
   reset: (state) => {
     state.isLoading = false
     state.isSuccess = false
     state.isError = false
     state.message = ''
   }
 },
 extraReducers: (builder) => {
   builder
     .addCase(register.pending, (state) => {
       state.isLoading = true
     })
     .addCase(register.fulfilled, (state, action) => {
       state.isLoading = false
       state.isSuccess = true
       state.user = action.payload
     })
     .addCase(register.rejected, (state, action) => {
       state.isLoading = false
       state.isError = true
       state.message = action.payload
       state.user = null
     })
     .addCase(login.pending, (state) => {
       state.isLoading = true
     })
     .addCase(login.fulfilled, (state, action) => {
       state.isLoading = false
       state.isSuccess = true
       state.user = action.payload
     })
     .addCase(login.rejected, (state, action) => {
       state.isLoading = false
       state.isError = true
       state.message = action.payload
       state.user = null
     })
     .addCase(logout.fulfilled, (state) => {
       state.user = null
     })
 }
})

export const {reset} = authSlice.actions
export default authSlice.reducer

In authSlice, all of our initial state and reducer functions will be defined.

import axios from 'axios'

const API_URL = '/api/users/'

// Register user
const register = async (userData) => {
 const response = await axios.post(API_URL, userData)

 if(response.data) {
   localStorage.setItem('user', JSON.stringify(response.data))
 }

 return response.data
}

// Login user
const login = async (userData) => {
 const response = await axios.post(API_URL + 'login', userData)

 if(response.data) {
   localStorage.setItem('user', JSON.stringify(response.data))
 }

 return response.data
}

// Logout user
const logout = () => {
 localStorage.removeItem('user')
}

const authService = {
 register,
 logout,
 login
}

export default authService

authService.js is strictly for making the http request and sending the data back and setting any data in localstorage.

Now we are adding proxy in package.json to resolve CORS issue

"proxy": "http://localhost:5000",

Install redux devTools chrome extension to debug application state’s changes.

Now import reducer function to a single store under app folder

// store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice'

export const store = configureStore({
 reducer: {
   auth: authReducer,
 },
});

Now start working on view components We’ll be using react-hooks and function-specific components here.

import {useState, useEffect} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import {useNavigate} from 'react-router-dom'
import {toast} from 'react-toastify'
import {FaUser} from 'react-icons/fa'
import {register, reset} from '../features/auth/authSlice'
import Spinner from '../components/Spinner'

function Register() {
 const [formData, setFormData] = useState({
   name: '',
   email: '',
   password: '',
   password2: ''
 })

 const {name, email, password, password2} = formData

 const navigate = useNavigate()
 const dispatch = useDispatch()

 const {user, isLoading, isError, isSuccess, message} = useSelector(
 (state) => state.auth)

   useEffect(() => {
     if(isError) {
       toast.error(message)
     }

     if(isSuccess || user) {
       navigate('/')
     }

     dispatch(reset())

   }, [user, isError, isSuccess, message, navigate, dispatch])

 const onChange = (e) => {
   setFormData((prevState) => ({
     ...prevState,
     [e.target.name]: e.target.value
   }))
 }

 const onSubmit = (e) => {
   e.preventDefault()

   if(password !== password2) {
     toast.error('Password do not match')
   } else {
     const userData = {
       name,
       email,
       password
     }

     dispatch(register(userData))
   }
 }

 if(isLoading) {
   return <Spinner/>
 }

 return (
   <>
     <section className='heading'>
       <h1>
         <FaUser/> Register
       </h1>
       <p>Please create an account</p>
     </section>

     <section className='form'>
       <form onSubmit={onSubmit}>
         <div className="form-group">
           <input type="text" className="form-control" id='name' name='name' value={name} placeholder='Enter your name' onChange={onChange}/>
         </div>
         <div className="form-group">
           <input type="email" className="form-control" id='email' name='email' value={email} placeholder='Enter your email' onChange={onChange}/>
         </div>
         <div className="form-group">
           <input type="password" className="form-control" id='password' name='password' value={password} placeholder='Enter password' onChange={onChange}/>
         </div>
         <div className="form-group">
           <input type="password" className="form-control" id='password2' name='password2' value={password2} placeholder='Confirm password' onChange={onChange}/>
         </div>
         <div className="form-group">
           <button type="submit" className='btn btn-block'>
             Submit
           </button>
         </div>
       </form>
     </section>
   </>
 )
}

export default Register

On submitting the form,we’re dispatching a register function which takes user data to authSlice.register() to register user data.

import {useState, useEffect} from 'react'
import {FaSignInAlt} from 'react-icons/fa'
import {useSelector, useDispatch} from 'react-redux'
import {useNavigate} from 'react-router-dom'
import {toast} from 'react-toastify'
import {login, reset} from '../features/auth/authSlice'
import Spinner from '../components/Spinner'

function Login() {
 const [formData, setFormData] = useState({
   email: '',
   password: '',
 })

 const {email, password} = formData

 const navigate = useNavigate()
 const dispatch = useDispatch()

 const {user, isLoading, isError, isSuccess, message} = useSelector(
   (state) => state.auth)

 useEffect(() => {
   if(isError) {
     toast.error(message)
   }

   if(isSuccess || user) {
     navigate('/')
   }

   dispatch(reset())

 }, [user, isError, isSuccess, message, navigate, dispatch])

 const onChange = (e) => {
   setFormData((prevState) => ({
     ...prevState,
     [e.target.name]: e.target.value
   }))
 }

 const onSubmit = (e) => {
   e.preventDefault()

   const userData = {
     email,
     password,
   }

   dispatch(login(userData))
 }

 if(isLoading) {
   return <Spinner/>
 }

 return (
   <>
     <section className='heading'>
       <h1>
         <FaSignInAlt/> Login
       </h1>
       <p>Login and start setting goals</p>
     </section>

     <section className='form'>
       <form onSubmit={onSubmit}>
         <div className="form-group">
           <input type="email" className="form-control" id='email' name='email' value={email} placeholder='Enter your email' onChange={onChange}/>
         </div>
         <div className="form-group">
           <input type="password" className="form-control" id='password' name='password' value={password} placeholder='Enter password' onChange={onChange}/>
         </div>
         <div className="form-group">
           <button type="submit" className='btn btn-block'>
             Submit
           </button>
         </div>
       </form>
     </section>
   </>
 )
}

export default Login

In the above code, the on form submit login function along with user data is dispatched and authenticated through a login function written in authSlice.js. If user data is valid, then user data is going to store in localstorage else, error messages will be displayed using toastify. Also, spinner is a component imported from the components folder.

Concurrently run frontend and backend

Install dev dependency package

npm i -D concurrently

After this, add extra scripts to run frontend and backend concurrently

"scripts": {
   "start": "node backend/server.js",
   "server": "nodemon backend/server.js",
   "client": "npm start --prefix frontend",
   "dev": "concurrently \"npm run server\" \"npm run client\"",
},

To runt the application in development mode, try following command

npm run dev

It will automatically open the application in the browser.

Conclusion

In this series of blogs, I try to give you a basic understanding of how a MERN stack app really works and what prerequisites are needed to build up a MERN stack application. If you thoroughly follow the steps, congratulations! You have built your first MERN application.

MERN stack is a collection of robust and scalable technologies that help you develop high-end web applications. Read about our collaboration with designerex to build the world’s largest designer dress-sharing platform. Need help with web application development? Talk to us!