Mastering User Authentication: Building a Simple Login Page using MERN Stack (Part 2 — Frontend)

Sanjana Shivananda
12 min readDec 21, 2023

--

Photo by Desola Lanre-Ologun on Unsplash

Introduction

Welcome to Part 2 of our comprehensive guide on crafting a user-friendly login page with the MERN stack: MongoDB, Express.js, React, and Node.js. In case you missed it, the previous segment, Part 1, covers the backend setup, ensuring a secure foundation for our login page. If you haven’t explored it yet, you can find it here to catch up.

In this part of the tutorial, we’ll delve into the pivotal process of seamlessly merging our robust backend with a dynamic frontend. We’ll take a closer look at building a simplistic yet effective login page using React, harnessing its powerful capabilities to design an interactive user interface. Additionally, we’ll explore leveraging CSS to elevate the visual aesthetics and usability of our login page.

Let’s dive into the integration of backend and frontend to create an engaging login experience!

Initializing and setting up a React app

The following steps illustrate the process of initializing and setting up a React application:

  1. Navigate to your desired directory for creating the React application in the terminal:
cd LoginPageApplication

2. Create the React app using the “create-react-app” command:

npx create-react-app frontend

3. Move into the app directory:

cd frontend 

4. Initiate the development server. The React app should automatically open in the browser on port 3000:

npm start

Note: Make sure to initiate the backend process in a separate terminal window or tab to ensure both the frontend and backend of the application are running concurrently.

File Structure

Before moving forward, it’s crucial to understand the file structure. While React generates several files, we’ll focus on those specifically highlighted in the image. The src directory houses the core code of the application.

Implementation

1. Integrating Backend to the Frontend

the initial step involves integrating the backend with the frontend through API calls. In a nutshell, APIs, or Application Programming Interfaces act as connectors allowing different software systems to communicate and interact. They define a set of rules and protocols that enable seamless data exchange and functionality between various components of an application.

For our integration, we’ll utilize Axios, a popular HTTP client library known for its simplicity and ease of use. It simplifies the process of making asynchronous HTTP requests to APIs, making it an excellent choice for frontend-backend communication.

To install Axios, use the following command:

npm install axios

Directory: LoginPageApplication/frontend/src/Backend.js

import axios from 'axios';

//USER AND AUTH ROUTES

//SIGNIN
export const signin = user => {
// API call to sign in a user
return axios.post("http://localhost:8000/api/signin", JSON.stringify(user), {
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(response => {
return response.data; // Return response data
})
.catch(err => {
return err.response.data; // Return error response data
})
}

//SIGNUP
export const signup = user => {
// API call to sign up a user
return axios.post("http://localhost:8000/api/signup", JSON.stringify(user),{
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(response => {
console.log(response.data);
return response.data; // Return response data
})
.catch(err => {
return err.response.data; // Return error response data
})
}

//SETTING THE JWT TOKEN IN USER'S BROWSER
export const authenticate = (data, next) => {
// Storing JWT token in user's browser
if(typeof window !== "undefined"){
localStorage.setItem("jwt", JSON.stringify(data));
next();
}
}

//SIGNOUT -> REMOVING JWT TOKEN
export const signout = (next) => {
// Removing JWT token upon signout
if (typeof window !== "undefined") {
localStorage.removeItem("jwt");

axios.get("http://localhost:8000/api/signout")
.then(response => {
console.log(response.data);
next();
})
.catch(err => console.log(err));
}
};

//VALIDATION IF USER IS SIGNED IN
export const isAuthenticated = () => {
// Checking if the user is authenticated
if (typeof window === "undefined") {
return false
}
if(localStorage.getItem("jwt"))
return JSON.parse(localStorage.getItem("jwt"));
else
return false
}

2. Now we build our Signup and Login Forms using JSX

In React, JSX allows us to use HTML-like components to build the website’s frontend. This means we can employ HTML tags within our React components to create the visual interface.

JSX (JavaScript XML) is an extension of JavaScript that allows us to write HTML-like code within React components. It simplifies the process of creating React elements by providing a familiar syntax similar to HTML.

For instance, in a React component, you can define the structure of your webpage by using JSX to create elements, similar to writing HTML tags. This makes it easier to visualize and organize the layout and content of the website.

Directory:
LoginPageApplication/frontend/src/Components/SignupPage/Signup.js

import React from "react";

// Signup component for the signup form
function Signup() {

return(
<div className='form-container'>
<div className='form-box'>
<h2>Create an account</h2>
{/* Input field for username */}
<div className='form-group'>
<label htmlFor="name">Username</label>
<input type="text" id="name" name="name" required/>
</div>
{/* Input field for email */}
<div className='form-group'>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required/>
</div>
{/* Input field for password */}
<div className='form-group'>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
{/* Signup button */}
<div className="form-group-button">
<button>Signup</button>
</div>
</div>
</div>
)
}

export default Signup;

Directory:
LoginPageApplication/frontend/src/Components/SigninPage/Signin.js

import React from 'react';

// Signin component for the login form
export function Signin(){

return(
<div className='form-container'>
<div className='form-box'>
<h2>Signin</h2>
{/* Input fields for email */}
<div className='form-group'>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required/>
</div>
{/* Input fields for password */}
<div className='form-group'>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
{/* Log in button */}
<div className="form-group-button">
<button>Log in</button>
</div>
{/* Message for redirection to signup */}
<div className='login-message'>
<center><p className='login_redirect mt-2'>Don't have an account?<b><a href='/signup'> Signup here</a></b></p></center>
</div>
</div>
</div>
)
}

export default Signin;

Directory:
LoginPageApplication/frontend/src/Components/Dashboard/Dashboard.js

import React from 'react';

// Dashboard component
function Dashboard(){
return(
// Displaying a heading for the dashboard
<h1>Dashboard</h1>
)
}

export default Dashboard;

3. Implementing Routing with React Router

In our React application, we define routes using react-router-dom, a popular routing library for React applications. React Router enables us to navigate between different components based on the URL, providing a seamless user experience within a single-page application (SPA).

Firstly, let’s install react-router-dom:


npm install react-router-dom

Directory:
LoginPageApplication/frontend/src/App.js

import './App.css';
import Signin from './Components/SigninPage/Signin.js'
import Signup from './Components/SignupPage/Signup.js';
import Dashboard from './Components/Dashboard/Dashboard.js';
import {Routes, Route, BrowserRouter as Router} from 'react-router-dom';

function App() {
return (
<div className='App'>
{/* Setting up the Router component from react-router-dom */}
<Router>
{/* Defining different Routes using Routes and Route components */}
<Routes>
{/* Route for the Dashboard component */}
<Route exact path='/' element={<Dashboard/>} />
{/* Route for the Login component */}
<Route exact path="/signin" element={<Signin/>} />
{/* Route for the Signup component */}
<Route exact path='/signup' element={<Signup/>} />
</Routes>
</Router>
</div>
);
}

export default App;

Now when we run our application, it should look something like this

4. Data Flow: Sending Frontend Data to the Backend

In React.js, states manage component-specific data changes, crucial for real-time updates and re-renders. States play a pivotal role in handling user input within our login and signup forms.

User-provided data, captured via React states, is transmitted to the backend through HTTP requests, predominantly POST requests, to “/api/signin” or “/api/signup” routes on the backend server.

The backend, powered by Express.js, processes incoming data, validating and interacting with the database as needed. Responses are sent back to the frontend, indicating the success or failure of the authentication or registration process.

This data flow between frontend and backend ensures a seamless exchange, enabling secure login functionality and data integrity. To enhance the user experience, we’ve incorporated loading indicators and error messages, providing users with feedback during the authentication or registration process.

Directory:
LoginPageApplication/frontend/src/Components/SignupPage/Signup.js

import React, {useState} from "react";
import { signup } from "../../Backend";

// Signup component for the signup form
function Signup() {

const [formValues, setFormValues] = useState({
email: "",
name: "",
password: "",
error: "",
loading: false,
success: false,
});

// Destructuring values from the state
const { name, email, password, error, loading, success } = formValues;

// Handles changes in the input fields
const handleChange = name => event => {
setFormValues({ ...formValues, error: false, [name]: event.target.value });
}

// Submits the form data to the backend
const onSubmit = async event => {
event.preventDefault();
setFormValues({ ...formValues, success: false, loading: true });

// Placeholder for the signup function calling the backend
signup({ name, email, password })
.then(data => {
if (data.error) {
setFormValues({ ...formValues, error: data.error, loading: false, success: false });
} else {
setFormValues({ ...formValues, success: true });
}
})
.catch();
}

// Displays error message if there's any
const errorMessage = () => {
return (
<div className='error-message' style={{ display: error ? "" : "none", color: "red" }}>
{error}
</div>
);
}

// Displays loading message during form submission
const loadingMessage = () => {
return (
loading && (
<div className="loading-message" style={{ display: error ? "" : "none", color: "red" }}>
<div className="loading-spinner"></div>
<p>Loading...</p>
</div>
)
);
}

// Displays success message upon successful form submission
const successMessage = () => {
return (
success && (
<div>
<center><p className='login_redirect mt-2'>Account created successfully <b><a href='/login'>Login here</a></b></p></center>
</div>
)
);
}



return (
<div className='form-container'>
<div className='form-box'>
<h2>Create an account</h2>
{errorMessage()}
{loadingMessage()}
{successMessage()}
<div className='form-group'>
<label htmlFor="name">Username</label>
<input type="text" id="name" name="name" onChange={handleChange("name")} required />
</div>
<div className='form-group'>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" onChange={handleChange("email")} required />
</div>
<div className='form-group'>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" onChange={handleChange("password")} required />
</div>
<div className="form-group-button">
<button onClick={onSubmit}>Signup</button>
</div>
</div>
</div>
);
}

export default Signup;

Similarly, we set up the Signin Form

Directory:LoginPageApplication/frontend/src/Components/SigninPage/Signin.js

import React, {useState} from 'react';
import { signin, authenticate } from '../../Backend';
import { Navigate } from 'react-router-dom';

// Signin component for the login form
export function Signin(){

// Initializing states for form fields, error, loading, and success messages
const [values, setValues] = useState({
email: "",
password: "",
error: "",
loading: false,
success: false,
});

// Destructuring values from the state
const { email, password, error, loading, success } = values;

// Handles changes in the input fields
const handleChange = name => event => {
setValues({ ...values, error: false, [name]: event.target.value });
}

// Submits the form data to the backend for user authentication
const onSubmit = async event => {
event.preventDefault();
setValues({ ...values, success: false, loading: true });
signin({ email, password })
.then(data => {
if (data.error) {
setValues({ ...values, error: data.error, loading: false, success: false });
} else {
authenticate(data, () => {
setValues({ ...values, success: true });
})
}
})
.catch();
}

// Displays error message if there's any
const errorMessage = () => {
return (<div className='error-message' style={{ display: error ? "" : "none", color: "red" }}>
{error}
</div>);
}

// Displays loading message during form submission
const loadingMessage = () => {
return (
loading && (
<div className="loading-message" style={{ display: error ? "" : "none", color: "red" }}>
<div className="loading-spinner"></div>
<p>Loading...</p>
</div>
)
);
}


return (
success ? <Navigate to="/" /> :
<div className='form-container'>
<div className='form-box'>
<h2>Signin</h2>
{loadingMessage()}
{errorMessage()}
<div className='form-group'>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" value={email} onChange={handleChange("email")} required />
</div>
<div className='form-group'>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" value={password} onChange={handleChange("password")} required />
</div>
<div className="form-group-button">
<button onClick={onSubmit}>Log in</button>
</div>
<div className='login-message'>
<center><p className='login_redirect mt-2'>Don't have an account?<b><a href='/signup'> Signup here</a></b></p></center>
</div>
</div>
</div>
)
}

export default Signin;

We've constructed a dashboard that presents a user-specific experience. Once a user is authenticated, the dashboard exhibits a personalized greeting along with a 'Sign Out' button, acknowledging the user's successful login. Additionally, the dashboard prominently displays the user's name, offering a tailored and welcoming interface

Directory:
LoginPageApplication/frontend/src/Components/Dashboard/Dashboard.js

import React from 'react';
import { isAuthenticated, signout } from '../../Backend';
import { useNavigate } from 'react-router-dom';

const Dashboard = () => {
const navigate = useNavigate(); // Initialize navigation
const authenticatedUser = isAuthenticated(); // Check if the user is authenticated

// Function to handle signout action
const onSignout = () => {
signout(); // Perform signout action
console.log("Signed out");
navigate('/signin'); // Redirect to login page after sign out
};

return (
!authenticatedUser ? <h1>Please sign in</h1> :
<div className='dashboard'>
<button onClick={onSignout}>Sign Out</button>
<h1>Hello, {authenticatedUser.user.name}</h1> {/* Display user's name */}
</div>
);
};

export default Dashboard;

This is what our UI looks like now upon successful login in

5. Styling the User Interface with CSS

To enhance the visual appeal and user experience of our application, we incorporate CSS (Cascading Style Sheets) to style our components. CSS allows us to customize the appearance of various elements within our frontend application, offering flexibility in designing layouts, colors, typography, and more.

By implementing CSS, we create an appealing and user-friendly interface for our login and signup forms, ensuring a visually engaging experience for our users.

Directory: LoginPageApplication/frontend/src/Components/SignupPage/Signup.css

body {
font-family: Arial, sans-serif;
}

.form-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f7f7f7;
}

.form-box {
padding: 2rem;
background-color: white;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.1);
width: 100%;
max-width: 500px;
}

h2 {
margin-bottom: 1.5rem;
text-align: center;
}

.form-group {
margin-bottom: 1.5rem;
}

label {
display: block;
margin-bottom: 0.5rem;
}

input {
width: 100%;
padding: 0.5rem;
margin-bottom: 1.5rem;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}

button {
background-color: #007bff;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
}

button:hover {
background-color: #0056b3;
}

.error-message {
margin-bottom: 1.5rem;
text-align: center;
}

.loading-message {
margin-bottom: 1.5rem;
text-align: center;
}

.loading-spinner {
width: 2rem;
height: 2rem;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Directory:
LoginPageApplication/frontend/src/Components/Dashboard/Dashboard.css

body {
font-family: Arial, sans-serif;
}

.form-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f7f7f7;
}

.form-box {
padding: 2rem;
background-color: white;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.1);
width: 100%;
max-width: 500px;
}

h2 {
margin-bottom: 1.5rem;
text-align: center;
}

.form-group {
margin-bottom: 1.5rem;
}

label {
display: block;
margin-bottom: 0.5rem;
}

input {
width: 100%;
padding: 0.5rem;
margin-bottom: 1.5rem;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}

button {
background-color: #007bff;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
}

button:hover {
background-color: #0056b3;
}

.error-message {
margin-bottom: 1.5rem;
text-align: center;
}

.loading-message {
margin-bottom: 1.5rem;
text-align: center;
}

.loading-spinner {
width: 2rem;
height: 2rem;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

you can import the CSS file directly into your JavaScript files to apply the styles accordingly. For instance:
import ‘./YourCSSFileName.css’;
This will link the CSS file with the respective JavaScript component, allowing the styles defined in the CSS file to be applied to the elements within that component.

Results

Conclusion

In conclusion, we’ve successfully built a user-friendly login application utilizing the MERN stack (MongoDB, Express.js, React, and Node.js). We’ve covered essential aspects such as setting up the backend, creating authentication functionalities, integrating frontend and backend logic, and designing a responsive interface using React components.

Moving forward, to enhance the application’s security, we can consider implementing OAuth (Open Authorization) for user authentication, which provides a standardized way to authorize access to user data without sharing credentials. This can be achieved by integrating OAuth providers like Google, Facebook, or GitHub for secure login and user verification.

Additionally, incorporating email verification during signup or two-factor authentication (2FA) can add an extra layer of security, ensuring that only authorized users access the application’s features.

If you’re interested in learning more about OAuth implementation, email verification, 2FA, or any other specific aspect of enhancing security in this application, please let me know. I’ll be happy to provide a tutorial on these topics!

P.S: You can check out the GitHub repo here: https://github.com/Sanjanaashivanand/LoginPageApplication

--

--

Sanjana Shivananda

I'm a computer science grad student currently diving into the fascinating world of Machine Learning.