How to Secure Routes in a React Application

Photo by RetroSupply on Unsplash

How to Secure Routes in a React Application

All react applications have routes. Sometimes, you preferably want some of those routes to only be accessible by a certain class of persons. These routes are known as protected routes and by the end of this article, you will learn how to build one to secure routes in your web apps.

Prerequisites

Before diving into this article, you should have a grasp of the concepts listed below:

  • Basics of using the react-router-dom library

  • The context API in react

  • The react useState hook

  • The react useEffect hook

If you've gotten all of those down, let's get into it.

Setting up a react project

To get started, you'll need a react app to work in. Spin up a react app by using the npx create-react-app command. Alternatively, you can choose to create a react project using vite which happens to be a lot faster than the now deprecated npx create-react-app. For further information on getting up and running with Vite, please check out the detailed docs. This tutorial will be using vite but feel free to use whatever you prefer.

The application you will be building will have three pages. A simplistic landing page, a login page and a protected dashboard page. Navigate to the App.jsx file and delete everything above and within the return statement. The App.jsx file should now look like this:

function App() {
  return;
}

export default App;

Delete the App.css file as that won't be required for this tutorial. In the index.css file, clear out the styles and replace them with these instead:

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
body {
  font-family: sans-serif;
  padding: 4rem;
}
.landing {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.login-link {
  background: black;
  color: white;
  padding: 1rem;
  border: 1px solid black;
  cursor: pointer;
  font-weight: 700;
  font-size: 1.2rem;
  border-radius: 12px;
  margin-top: 2rem;
  display: inline-block;
}
.login-btn {
  background: white;
  color: black;
  padding: 1rem;
  border: 1px solid black;
  cursor: pointer;
  font-weight: 700;
  font-size: 1.2rem;
  border-radius: 12px;
  margin: 4rem 0 4rem 0;
}
.dashboard {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
.login-container {
  border: 1px solid gray;
  padding: 4rem;
  border-radius: 12px;
}
.input-control {
  display: flex;
  gap: 0.5rem;
}

We will create three components to represent the three routes of the app. Create three new files. Name the first Landing.jsx, the second Login.jsx and the third Dashboard.jsx. In the Landing.jsx file, create a Landing component and export same.

const Landing = () => {
  return;
};

export default Landing;

In Landing's return statement, add in the following jsx to flesh out the landing page.

<div className="landing">
  <div>
    <h1>
      Welcome to task rabbit! A task management app like no other. Login to see
      your pending tasks.
    </h1>
    <button className="login-link">Login</button>
  </div>
</div>

Similarly, create a Login component and add in the following jsx to its return statement:

const Login = () => {
  return (
    <div className="login-container">
      <div className="input-control">
        <label htmlFor="email">Email</label>
        <input type="text" placeholder="Enter your email" name="email" />
      </div>
      <div className="input-control">
        <label htmlFor="password">Password</label>
        <input
          type="password"
          placeholder="Enter your password"
          name="password"
        />
      </div>
    </div>
  );
};

export default Login;

The dashboard, however, will just be a div with a simple "Welcome to your dashboard" message at the center of the screen

const Dashboard = () => {
  return (
    <div className="dashboard">
      <h1>Welcome to your dashboard</h1>
    </div>
  );
};

export default Dashboard;

Adding routes using react-router-dom

Routing in this tutorial will be handled by the react-router-dom library. Open the terminal and type the command npm install react-router-dom to install the library into your project.

Configuring react-router-dom, requires you to tweak your main.jsx file (or index.js if you're using the create-react-app CLI) to change a few things. In the main.jsx file, change the <React.StrictMode> component that wraps the App component to <BrowserRouter>. After that tweak, the main.jsx file should then look like this:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

With react-router-dom now properly installed and configured, it's time to define the various routes for the application. There will be three routes. The "/" route will render the Landing component, while the "/login" and "/dashboard" routes render the Login and Dashboard components respectively.

In the App component, at the top of the file, import the Routes and Route component from the react-router-dom library.

import { Routes, Route } from "react-router-dom";

Then, in the App component's return statement, add in the various routes like so:

<Routes>
  <Route path="/" element={<Landing />} />
  <Route path="/login" element={<Login />} />
  <Route path="/dashboard" element={<Dashboard />} />
</Routes>

Finally, we'll modify the Landing component to link to our newly defined routes.

import { Link } from "react-router-dom";

const Landing = () => {
  return (
    <div className="landing">
      <div>
        <h1>
          Welcome to task rabbit! A task management app like no other. Login to
          see your pending tasks.
        </h1>
        <Link to="/login" className="login-link">
          Login
        </Link>
      </div>
    </div>
  );
};

export default Landing;

With the jsx template done, open a terminal session and run the command npm run dev if you're using vite or npm start if you're using CRA to spin up a local development server. Previewing the app in a browser should look like this:

With the various routes now defined, it's about time we get into creating the actual protected route component.

Creating a protected route component

The protected route component works quite simply. It takes its children as props, checks whether a user is signed in then renders out its children if a user is signed in. Where a user is not signed in, the component redirects the user to a different, public route. Let's first sketch out the component.

const ProtectedRoute = ({ children }) => {
  return children;
};

export default ProtectedRoute;

As of yet, the component does nothing more than render out its children, we'll get back to that in a bit. Before we can get into creating the logic for checking if a user is signed in, we'll need to create some sort of mock authentication, to make sure everything's working as intended. The signin state will be stored in a global state, isSignedIn, inside a context so it can be accessed anywhere in the component tree.

Create a new file and name it AuthContext.jsx then paste in the following code to create the context:

import { createContext, useState } from "react";

export const AuthContext = createContext(null);

const AuthContextProvider = ({ children }) => {
  const [isSignedIn, setIsSignedIn] = useState(false);

  return (
    <AuthContext.Provider value={{ isSignedIn, setIsSignedIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContextProvider;

In the App component, wrap everything within the return statement with the AuthContextProviderComponent:

<AuthContextProvider>
  <Routes>
    <Route path="/" element={<Landing />} />
    <Route path="/login" element={<Login />} />
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</AuthContextProvider>

Don't forget to import it at the top like so:

import AuthContextProvider from "./contexts/AuthContext";

The isSignedIn state should change whenever a user logs in by clicking the login button. To achieve this behavior, modify the Login component to this:

import { useContext } from "react";
import { useNavigate } from "react-router-dom";
import { AuthContext } from "./contexts/AuthContext";

const Login = () => {
  // destructuring the setIsSignedIn function from the AuthContext
  const { setIsSignedIn } = useContext(AuthContext);

  // useNavigate hook to navigate to the dashboard page
  const navigate = useNavigate();

  // function to set signed in state to true
  const handleLogin = () => {
    setIsSignedIn(true);
    // navigates to the dashboard after signin is complete
    navigate("/dashboard");
  };

  return (
    <div className="login-container">
      <div className="input-control">
        <label htmlFor="email">Email</label>
        <input type="text" placeholder="Enter your email" name="email" />
      </div>
      <div className="input-control">
        <label htmlFor="password">Password</label>
        <input
          type="password"
          placeholder="Enter your password"
          name="password"
        />
      </div>
      <button onClick={handleLogin}>Login</button>
    </div>
  );
};

export default Login;

P.S: In a real application, the div with a class of login container would probably be a form and you'd use the onSubmit event on the form itself to handle form submission. Form validation is also something you'd want to add.

With the mock authentication taken care of, let's get back to building the ProtectedRoute component. To get access to the isSignedIn state, we will use a useContext hook inside the component.

import { AuthContext } from "./contexts/AuthContext";
import { useContext } from "react";

const ProtectedRoute = ({ children }) => {
  const { isSignedIn } = useContext(AuthContext);
  return children;
};

export default ProtectedRoute;

With the signedin state now available locally, we can now proceed to bullding the logic for checking whether a user is currently signed in or not.

import { AuthContext } from "./contexts/AuthContext";
import { useEffect } from "react";
import { useContext } from "react";
import { useNavigate } from "react-router-dom";

const ProtectedRoute = ({ children }) => {
  // consuming the context
  const { isSignedIn } = useContext(AuthContext);
  const navigate = useNavigate();

  useEffect(() => {
    // checks whether isSignedin is false. If so, navigates to the login page.
    if (!isSignedIn) {
      navigate("/login");
    }
  }, []);

  // if isSignedIn is true, the component renders out its children.
  return children;
};

export default ProtectedRoute;

The check is done within a useEffect hook, to ensure the component is mounted before the check runs.

With that, we have successfully created a ProtectedRoute component that only renders out its children when a user is currently signed in. Next up is using the thing.

Using the protected route

Using the protected route is quite easy. Any component that needs to be protected is simply wrapped in the ProtectedRoute component. Since the dashboard route is what we will be protecting in this tutorial, modify the route to the dashboard page inside the App.jsx file to this:

<Route
  path="/dashboard"
  element={
    <ProtectedRoute>
      <Dashboard />
    </ProtectedRoute>
  }
/>

Don't forget to import the component at the top of the file. Opening the browser and trying to navigate to the dashboard component now redirects to the login page. Unauthenticated users can no longer access the dashboard!

Conclusion

This article has been a short introduction into how react developers can secure routes in their applications using a custom ProtectedRoute component. I hope this article helped you learn a lot about securing routes in your appliactions.

Thanks for sticking to the end and happy hacking!