January 10, 2024| 0 minute read

How to build a simple authentication app with React Context and Router

Tutorial Image

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 from react 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 updating authState
    • The isUserAuthenticated property is used for determining if a user session states whether login or logout
    • The logout method resets the setAuthState to empty object {} and redirects to the login page
  • 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, and useNavigation 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 using useState, then create the login, isUserAuthenticated and logout 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 the logout method updates the authentication state variables to indicate that the user is logged out. The isUserAuthenticated method returns a boolean value indicating whether the user is authenticated or not. The boolean value returned by isUserAuthenticated is derived from checking if the return value from mock.login method is a user object or {} empty object.

  • Then, the export the AuthProvider component by including it with the AuthContext 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 as Router, and use it to wrap AuthProvider and App 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 and Route 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 NOT userID in the UserDetails page as described in setp 13 below.

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 and useNavigation hooks again which will be used to get the authContext.login method for checking the authentication state and the useNavigation for redirecting to the Dashboard page if the authentication state is true respectively. Then we will create a form with one input field for username, and handle username change with the onChange event handler, then use the onSubmitevent 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 the useNavigate hook again. This time to naviage to the user's requested last page or Dashboard route path / if state?.path is undefined. Remember the user's last requested page was stored in state?.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

Want more tutorials?

Subscribe and get notified whenever new tutorials get added to the collection.

By submitting this form, I agree to cajieh.com Privacy Policy.