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!