How to build a simple authentication app with React Context and Router
Source: www.pixabay.com
Introduction
In this tutorial, we’ll build a simple authentication application in React using React Context and Router DOM with the following functionalities including Login, logout, protected routes, and redirects to the last requested page or Not-Found user page if an invalid user ID is entered in the browser address bar.
We’ll create a few components and use React ContextAPI to manage the application authentication state. Then, we'll leverage React Router DOM library components to handle protected routes and redirect to the last requested page. See the diagram below of how this works.
Note: The application authentication state would not persist on page refresh in this tutorial. Also, note that this is a basic example and doesn't cover core security aspects. In a real-world application, you would typically use a server-side authentication mechanism and handle user sessions.
Let’s get started by following the steps below.
Pre-requisite:
- Basic knowledge of Javascript and React is required
- A computer system with Node.js runtime engine installed
- Proficient in the use of any modern code editors i.e. VS Code
Step #1 Set up a new React application project
- Create a new React application project using
create-react-app
by running the command below in the terminal:
npx create-react-app simple-auth-app
- Open the project in a code editor, then start the development server with the command below
npm run dev
See our tutorial on How to set up a react application project development environment for details.
Step #2 Create an imaginary auth mock server
- Locate the src folder, then create a new file called mock-auth-server.js inside it
- Create an array of objects for storing users' data as shown below:
const users = [
{
id: '1',
email: 'john.thomas@test.com',
firstName: 'John',
lastName: 'Thomas',
status: 'Active',
},
{
id: '2',
email: 'rebecca.grace@test.com',
firstName: 'Rebecca',
lastName: 'Grace',
status: 'In-active',
},
{
id: '3',
email: 'peter.dan@test.com',
firstName: 'Peter',
lastName: 'Dan',
status: 'Active',
},
]
- Create a mock auth object, then add a login method to mimic an API server for authentication, and getUsers and getUser methods to fetch users and user data respectively as shown below:
export const mockAuth = {
login(email) {
const user = users.find((u) => u.email === email);
return user;
},
getUsers() {
return users;
},
getUser(id) {
const user = users.find((user) => user.id === id);
return user;
},
};
In the code above, the login method uses find
method to check for the user email in the users' array of objects and return the user object if it exists, else it returns an empty object
Note: The mockAuth
object is an exported object since it will be using it outside the module.
Step #3 Create the authentication app context
- Create a new file called AuthContext.jsx inside the src folder. In the AuthContext.jsx, import
createContext
method fromreact
as shown below:
import { createContext } from "react";
- Create an initial user object with the properties as shown below:
const initialUserState = {
user: {
email: "",
firstName: "",
lastName: "",
id: 0,
},
};
- Create the React context using
createContext()
method, and set the initial properties with default values as shown below:
const AuthContext = createContext({
authState: initialUserState,
setAuthState: () => initialUserState,
isUserAuthenticated: () => false,
});
-
In the code above:
- The
authState
is an object containing auth user properties - The
setAuthState
method is used for updatingauthState
- The
isUserAuthenticated
property is used for determining if a user session states whether login or logout - The
logout
method resets thesetAuthState
to empty object{}
and redirects to the login page
- The
-
Export the
AuthContext
as shown below:
export { AuthContext };
Step #4 Create the authentication provider component
- Inside the src folder again, create a new file called “AuthProvider.jxs”. In AuthProvider.jxs, import
useState
, anduseNavigation
as shown below:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
- Next import the
mockAuth
from "./mock-auth-server" as show below:
import { mockAuth } from "./mock-auth-server";
- Create a function component called AuthProvider, initialize the
useNavigation
, and define the state variables for authentication usinguseState
, then create thelogin
,isUserAuthenticated
andlogout
methods. - Next, warp the component's children with the
AuthContext.Provider
component and pass the authentication state variables as the value prop as shown below:
const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const [authState, setAuthState] = useState({});
const [isError, setIsError] = useState(false);
const isUserAuthenticated = () =>
authState && Object.keys(authState)?.length > 0 ? true : false;
const login = (username) => {
const user = mockAuth.login(username);
setAuthState(user);
setIsError(!isUserAuthenticated());
};
const logout = () => {
setAuthState({});
setIsError(false);
};
return (
<AuthContext.Provider
value={{
authState,
isUserAuthenticated,
login,
logout,
isError,
}}
>
{children}
</AuthContext.Provider>
);
};
-
In the code above, the
login
method updates the authentication state variables to indicate that the user is logged in while thelogout
method updates the authentication state variables to indicate that the user is logged out. TheisUserAuthenticated
method returns a boolean value indicating whether the user is authenticated or not. The boolean value returned byisUserAuthenticated
is derived from checking if the return value frommock.login
method is auser
object or{}
empty object. -
Then, the export the
AuthProvider
component by including it with theAuthContext
exported earlier.
export { AuthProvider, AuthProvider };
Step #5 Use the authentication context in the application entry point
- In your main.jsx file, import the
AuthProvider
component from “AuthProvider.js” as shown below:
import { AuthProvider } from "./AuthProvider";
- Next wrap the App component with the
AuthProvider
component to provide the authentication context to all child components of the App component in the application as shown below:
<AuthProvider>
<App />
</AuthProvider>
Step #6 Create the application routes
To use React Router Dom for implementing application routing, we’ll have to install it first using a tool such as npm
- Install
react-router-dom
dependency to access the React Router components as shown below:
npm install react-router-dom
-
Create the five components for the application namely Dashboard, Login, UserDetails, ProtectedRoute, and Nav.
-
Import the React Router DOM parent compponent called the
BrowserRouter
asRouter
, and use it to wrapAuthProvider
andApp
components in the "main.jsx" file as shown below:
import { BrowserRouter as Router } from "react-router-dom";
<Router>
<AuthProvider>
<App />
</AuthProvider>
</Router>
- Next, in App.jsx, import
Routes
andRoute
from React Router DOM library and create routes for Login, Dashboard and UserDetails pages as shown below:
import { BrowserRouter as Router } from 'react-router-dom';
<Routes>
<Route
path="/"
element={
<Dashboard />
}
/>
<Route
path="/userdetails/:id"
element={
<UserDetails />
}
/>
<Route path="/login" element={<Login />} />
<Route path="*" element={<Login />} />
</Routes>
- Note:
- The UserDetails component route path - "/userdetails/:id" is parameterized because the content of this page would be dynamically determined by the
userID
passed in. - The path set to
*
will redirects to Login page if the user tries to enter invalid routes NOTuserID
in the UserDetails page as described in setp 13 below.
- The UserDetails component route path - "/userdetails/:id" is parameterized because the content of this page would be dynamically determined by the
Step #7 Implement the logic and functionality for the reusable Protected component
-
Since there will be more than one private route/page, ideally it is best practice to create a reusable component to reduce code duplication and improve code quality and future maintenance. So that is exactly what we’ll do, create a reusable Protected component to protect private routes in the application. The Protected component will check if the user is authenticated and render the specified component, or redirect to a login page if not.
-
We have to install an additional npm library because of two other components needed in the Protected component using a tool such as npm Install react-router dependency to access the
useLocation
and,Navigate
components as shown below: npm install react-router See the complete code for the reusable Protected component below:
import { useLocation, Navigate } from "react-router";
import { useContext } from "react";
import { AuthContext } from "../AuthContext";
function Protected({ children }) {
const { isUserAuthenticated } = useContext(AuthContext);
const location = useLocation();
return !isUserAuthenticated() ? (
<Navigate to="/login" replace state={{ path: location.pathname }} />
) : (
children
);
}
export default Protected;
In the code above, the Protected component takes children
props that would be rendered if the user is authenticated.
The useContext
hook lets you read and retrieve the current value of a context within a functional component. We’ll use the useContext
hook to get isUserAuthenticated
context value. The isUserAuthenticated
property is a boolean value indicating whether the user is authenticated or not. If isUserAuthenticated
true
returns the children
which is a private component else redirect and return the Login page.
The useLocation
hook returns the current location
object which contains the pathname
property of the current route URL. In the code above, we used the Navigate
component from react-router
to navigate programmatically to Login page if isUserAuthenticated
property is false
. In the Navigate
, we assigned the location.pathname
property to the state.path
property, and then pass replace
to replace the current route URL entry in the history stack with the new one so that whenever the user enters something in the browser address bar the application could store the user's requested last page before logging out.
Note: The replace
property have default value to true
and it is same as replace:true
.
the user is in which would be stored in the state.path
property so that whenever the user enters something in the browser address bar
the application could remember the user's requested last page before logging out, and the stored route URL will be use to redirect the user to that page if the user logs in again. We’ll see how to retrieve the state.path
property and redirect to the user's requested last page later on.
In the next step, the stored route URL will be use to redirect the user to last requested page if the user logs in again. Then in step 12, we’ll use the reusable Protected component to protect private routes in the application.
Step #8 Implement the logic and functionality for the Login component
- In the Login.js component, we will instantiate the
useContext
anduseNavigation
hooks again which will be used to get theauthContext.login
method for checking the authentication state and theuseNavigation
for redirecting to the Dashboard page if the authentication state istrue
respectively. Then we will create a form with one input field for username, and handle username change with theonChange
event handler, then use theonSubmit
event handler function for the form submission as shown in the code below:
import { useLocation, useNavigate } from "react-router";
import { useState, useContext } from "react";
import { AuthContext } from "../AuthContext";
const dummyUsers = [
'john.thomas@test.com',
'rebecca.grace@test.com',
'peter.dan@test.com',
'invalid.username@test.com'
]
const Login = () => {
const authContext = useContext(AuthContext);
const [username, setUsername] = useState("Choose user");
const navigate = useNavigate();
const { state } = useLocation();
const handleLogin = (e) => {
e.preventDefault();
authContext.login(username);
navigate(state?.path || "/");
};
const onLoginChange = (value) => {
setUsername(value);
};
return (
<div>
<h1>Login page</h1>
{authContext.isError && <p style={{color: "red"}}>Failed: Invalid username!</p>}
<form aria-label="Default select example" onSubmit={handleLogin}>
<select
value={username}
onChange={(e) => onLoginChange(e.target.value)}
>
<option value={username} disabled>
{username}
</option>
{dummyUsers.map((el) => {
return (
<option value={el} key={el}>
{el}
</option>
);
})}
</select>
<button disabled={username === "Choose user"}>Login</button>
</form>
</div>
);
};
export default Login;
- In the
handleLogin
method, we make use of theuseNavigate
hook again. This time to naviage to the user's requested last page or Dashboard route path/
ifstate?.path
isundefined
. Remember the user's last requested page was stored instate?.path
in the previous step, so we are retrieving it here and redirecting to the stored route URL which is the user's last requested page.
Step #9 Implement the logic and functionality for the Dashboard component
- In the Dashboard.js component, implement the code logic for fetching users and displaying the users. Next, we’ll use the map function to iterate over an array of users objects and render each user's information as shown below:
import { Link } from "react-router-dom";
import { mockAuth } from "../mock-auth-server";
const Dashboard = () => {
const users = mockAuth.getUsers();
return (
<div>
<h4>Dashboard page</h4>
<table>
<thead>
<tr>
<th>UserID</th>
<th>Firstname</th>
<th>Lastname</th>
<th></th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.id} </td>
<td>{user.firstName} </td>
<td>{user.lastName}</td>
<td>
{" "}
<Link to={`/userdetails/${user.id}`}>View Details </Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Dashboard;
In the above code, we used the Link component from React Router DOM to dynamically link each user displayed on the Dashboard to the User Details page by accessing the user.id
of each user using template literal. In step 11 below, we’ll use the user.id
to fetch the user detail information and display them accordingly.
Step #10 Implement the logic and functionality for the Nav component
In the Nav component, again we will instantiate the useContext
hook which will be used to get the authContext
and get the authState
and logout
props from it using object destructuring. Then create two navigation links namely Dashboard and Logout using Link component from React Router DOM.
The Dashboard link would be for navigating to the Dashboard page if the user is on the UserDetail page, while the Logout link is for logging out the application. On logout, the user would be redirected to the Login page. To view the user details, the user will have to be authenticated first, that is logged in, and taken to the Dashboard, then can click on any of the users to view details. See the implementation for the Nav component below:
import { Link } from "react-router-dom";
import { useContext } from "react";
import { AuthContext } from "../AuthContext";
const Nav = () => {
const { authState, logout } = useContext(AuthContext);
return (
<header>
<h2>
<Link className="navbar-brand" to={"/"} alt="logo">
How to handle redirecting to the last requested page in React
</Link>
</h2>
<p>Hi {authState.firstName}, You are logged in.</p>
<nav
className="me-auto my-2 my-lg-0"
style={{ maxHeight: "100px" }}
>
<Link className="nav-link" to={"/"}>
Home
</Link>
{" "} | {" "}
<Link
className="nav-link"
to={"/login"}
onClick={() => logout()}
>
Logout
</Link>
</nav>
</header>
);
};
export default Nav;
Note: The Link component for Logout has the onClick
event handler which triggers the logout() method retrieved form AuthContext
. Also, we are displaying the logged-in user.firstname
with authState.firstName
.
Step #11 Use the Nav component in the App component
In the App Component, we'll conditionally render the Nav component in the App component if the isUserAuthenticcated
is true
so that it will be shown on all protected routes/pages as shown in the code below:
{isUserAuthenticated() && (
<Nav username={authState?.firstName} logout={logout} />
)}
In the next step, we’ll see the complete code for the App component with the Nav component placed above the Routes components.
Step #12 Wrap the private routes with the Protect component
In this step, we’ll wrap the Dashboard and UserDetails routes/components with the Protect component as shown below:
import { useContext } from "react";
import { Routes, Route } from "react-router-dom";
import "./App.css";
import { AuthContext } from "./AuthContext";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import UserDetails from "./pages/UserDetails";
import Protected from "./components/Protected";
import Nav from "./components/Nav";
function App() {
const { isUserAuthenticated } = useContext(AuthContext);
return (
<div className="app">
{isUserAuthenticated() && (
<Nav />
)}
<Routes>
<Route
path="/"
element={
<Protected>
<Dashboard />
</Protected>
}
/>
<Route
path="/userdetails/:id"
element={
<Protected>
<UserDetails />
</Protected>
}
/>
<Route path="/login" element={<Login />} />
</Routes>
</div>
);
}
export default App;
Step #13 Implement the logic and functionality for the UserDetails component
In the UserDetails component, we want to display the data for a single user which could be accessed by clicking the View Details button on the Dashboard or entering the
userID
in the browser URL if known. Remember in step 10 above, we created a link to User Details from Dashboard, so we are going to use the useParams
hook from 'react-router-dom' to access the dynamic id
parameter from the URL which is the userID
in our case. Then, use that userID
to get the user details information and display them accordingly as shown below:
import { useParams } from "react-router-dom";
import { mockAuth } from "../mock-auth-server";
const UserDetails = () => {
const { id } = useParams();
const user = mockAuth.getUser(id);
return (
<div>
<p>
<strong>User ID: </strong> {user.id}{" "}
</p>
<p>
<strong>Firstname: </strong> {user.firstName}{" "}
</p>
<p>
<strong>Lastname: </strong> {user.lastName}{" "}
</p>
<p>
<strong>Status: </strong> {user.status}{" "}
</p>
</div>
);
};
export default UserDetails;
Step #14 Implement the logic and functionality for handling the not found user error
First, let's test if a user tries to enter an invalid user from the browser address bar on the user details page. To test, we’ll log in and go to the user details page, then change URL ID parameter to something else not in the user's data, for example, 5, and hit enter. The application logs out. Remember, the authentication state is not persistent, Try to log in again, the application throws an error.
To handle this error thrown if a user types in an invalid user_id
in the browser address bar on the user details page and tries to log in again, we’ll get the value of user_id
and check if the user_id
exists in the mock user's data, then if theuser_id
is not found in the mock users' data, we’ll show a user not found message else display the user details information. See the logic below:
if (!user) {
return <h3>{`The userID "${id}"" is invalid. Click Home to view vaild UserID.`} </h3>;
}
Add the code above before the return
statement in the UserDetails component.
In the code, whenever the user types something in the address bar, hit the enter key and the action refreshes the page, then logs the user out. If the user tries to log in again, we perform a check for the user passing the user_id
in the getUser
method, if the user doesn’t exist, the getUser
returns undefined
. Next, we perform an if
conditional statement check by negative the user value like this !user
, if the condition is true
, return and show the Non-Found User message else return and show user details information.
Conclusion
That’s it on how to build a simple authentication application using React Context and Router DOM with features such as login, logout, protected routes, and redirects to the last requested page or Not-Found user page if an invalid user ID is entered in the browser address bar.
I hope this helps! Go to the contact page and let me know if you have any further questions.
Happy coding!
Complete code
To see the complete project setup and code, go to the Github repo: simple-auth-app