Using React Router v6 for Private and Public Routes with Access Validation

Written by ljaviertovar | Published 2023/08/30
Tech Story Tags: react | react-tutorial | javascript | javascript-development | web-development | programming | front-end-development | frontend

TLDRvia the TL;DR App

As we build interactive web applications using React and React Router, security becomes a crucial aspect to consider. It is essential to ensure that only authorized users can access sensitive information by protecting certain routes.

While there are various mechanisms--such as access tokens or role management libraries--to implement a comprehensive authentication system, in this tutorial, we will focus on implementing route access validation using React Router.

We will build a simple application where we’ll learn how to set up private and public routes and redirect users when they try to access unauthorized routes. Basic knowledge of React Router is required to follow this tutorial.

What is React Router?

In essence, React Router is a routing library designed for React-based web applications. Its main purpose is to facilitate navigation between components or views in a single-page application (SPA) declaratively.

Instead of having multiple separate HTML pages, React Router allows us to create a seamless navigation experience within a single web page.

With this library, we can define routes and associate them with specific components. This way, when users access a particular URL, the corresponding component is dynamically displayed without reloading the entire page.

Set up React Router

We create a new React project with Vite and follow the steps indicated. This time, we will use pnpm, you can use the package manager of your choice.

pnpm create vite

We install the dependencies that we will need in the project:

pnpm install react-router-dom

After that, we will create the following structure for the project:

...
├── src/
│   ├── components/
│   │   ├── layout/
│   │   │   └── Nav.jsx
│   │   ├── LoginButton.jsx
│   │   └── ProtectedRoute.jsx
│   ├── pages/
│   │   ├── AdminPage.jsx
│   │   ├── ConfigPage.jsx
│   │   ├── DashboardPage.jsx
│   │   ├── HomePage.jsx
│   │   ├── LandingPage.jsx
│   │   └── index.js
│   │   ...
│   ├── App.jsx
...

Defining Routes

App.jsx :

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

import { AdminPage, ConfigPage, DashboardPage, HomePage, LandingPage } from "./pages"

import Nav from "./components/layout/Nav"

import "./App.css"

function App() {
 const [user, setUser] = useState(null)

 return (
  <BrowserRouter>
   <Nav user={user} setUser={setUser} />

   <Routes>
    <Route index element={<LandingPage />} />
    <Route path='/landing' element={<LandingPage />} />
    <Route path='/home' element={<HomePage />} />
    <Route path='/dashboard' element={<DashboardPage />} />
    <Route path='/admin' element={<AdminPage />} />
    <Route path='/config' element={<ConfigPage />} />
   </Routes>
  </BrowserRouter>
 )
}

export default App

In the App.js component, we establish the navigation and routing structure of the application using react-router-dom and define the different pages to be rendered for each specific route.

We also use the user state to store and update user information when logging in and out. For this tutorial, we will simply pass these properties down to child components.

We have also added the Nav component, which is displayed at the top of all pages and provides a navigation bar.

Nav.jsx :

import { Link } from "react-router-dom"
import LoginButton from "../LoginButton"

export default function Nav({ user, setUser }) {
 return (
  <nav>
   <ul>
    <li>
     <Link to='/landing'>Landing</Link>
    </li>
    <li>
     <Link to='/home'>Home</Link>
    </li>
    <li>
     <Link to='/admin'>Admin</Link>
    </li>
    <li>
     <Link to='/config'>Config</Link>
    </li>
    <li>
     <Link to='/dashboard'>Dashboard</Link>
    </li>
    <li>
     <LoginButton user={user} setUser={setUser} />
    </li>
   </ul>
  </nav>
 )
}

In the Nav component, we import the LoginButton component, which we will use to simulate the login and logout.

LoginButton.jsx :

import { useState } from "react"

const USER_DUMMY1 = {
 id: 1,
 name: "John",
 permissions: ["admin", "dashboard"],
 roles: ["admin"],
}

const USER_DUMMY2 = {
 id: 2,
 name: "John",
 permissions: ["admin"],
 roles: ["admin"],
}

const USER_DUMMY3 = {
 id: 3,
 name: "John",
 permissions: ["dashboard"],
 roles: [],
}

export default function LoginButton({ user, setUser }) {
 const [userLogged, setUserLogged] = useState(null)

 const login = () => {
  setUser(userLogged)
 }

 const logout = () => {
  setUser(null)
  setUserLogged(null)
 }

 return (
  <div>
   {!user ? <button onClick={() => login()}>Login</button> : <button onClick={() => logout()}>Logout</button>}
   {!user && (
    <div>
     <button
      className={userLogged?.id === 1 && "active"}
      onClick={() => {
       setUserLogged(USER_DUMMY1)
      }}
     >
      User 1
     </button>
     <button className={userLogged?.id === 2 && "active"} onClick={() => setUserLogged(USER_DUMMY2)}>
      User 2
     </button>
     <button className={userLogged?.id === 3 && "active"} onClick={() => setUserLogged(USER_DUMMY3)}>
      User 3
     </button>
    </div>
   )}
  </div>
 )
}

This component contains information for three sample users, each with different permissions and roles. This allows us to verify the correct validation of the routes.

This component should contain all the user authentication logic. However, in this case, we will only simulate the login process.

To visually test our protected routes, we have added a series of buttons that allow us to choose a test user to log in and check which pages they can access.

With this, we have created an application with simple routing, implemented routes, a navigation bar, and a login feature.

Now, it’s time to validate the routes for each user.

Protecting the routes

One way to protect routes is by using the concept of “nested routing” or “wrapper routing.”

This involves wrapping a route within another route so that the wrapping route takes care of validating if the user is authenticated and has permission to access the inner route.

Otherwise, the user can be redirected to another route, or the corresponding action can be taken based on your project’s needs. This technique allows effective control over access to certain routes and ensures the security of your application.

ProtectedRoute.jsx :

import { Navigate, Outlet } from "react-router-dom"

export default function ProtectedRoute({ isAllowed, redirectTo = "/landing", children }) {
 if (!isAllowed) {
  return <Navigate to={redirectTo} />
 }
 return children ? children : <Outlet />
}

Inside ProtectedRoute, we define a function with three properties: isAllowedredirectTo, and children.

If isAllowed is false (meaning the user is not permitted to access the route), it returns a Navigate component that redirects the user to the route specified by redirectTo, which defaults to /landing.

If isAllowed is true (meaning the user has permission to access the route), the following code is executed:

  • It checks if the children prop exists (child elements passed to the ProtectedRoute component).

  • If there are children, it returns children, which means the child components will be rendered within ProtectedRoute.

  • If there are no children, it returns the Outlet component, which allows the nested routes to be rendered in that place.

Once we have the component that will protect the routes, we need to update the router of the application:

App.jsx :

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

import { AdminPage, ConfigPage, DashboardPage, HomePage, LandingPage } from "./pages"
import ProtectedRoute from "./components/ProtectedRoute"
import Nav from "./components/layout/Nav"

import "./App.css"

function App() {
 const [user, setUser] = useState(null)
 return (
  <BrowserRouter>
   <Nav user={user} setUser={setUser} />
   <Routes>
    <Route index element={<LandingPage />} />
    <Route path='/landing' element={<LandingPage />} />
    <Route element={<ProtectedRoute isAllowed={!!user} />}>
     <Route path='/home' element={<HomePage />} />
     <Route
      path='/dashboard'
      element={
       <ProtectedRoute isAllowed={!!user && user.permissions.includes("dashboard")} redirectTo='/home'>
        <DashboardPage />
       </ProtectedRoute>
      }
     />
     <Route
      path='/admin'
      element={
       <ProtectedRoute isAllowed={!!user && user.roles.includes("admin")} redirectTo='/home'>
        <AdminPage />
       </ProtectedRoute>
      }
     />
     <Route
      path='/config'
      element={
       <ProtectedRoute isAllowed={!!user && user.roles.includes("admin")} redirectTo='/home'>
        <ConfigPage />
       </ProtectedRoute>
      }
     />
    </Route>
   </Routes>
  </BrowserRouter>
 )
}
export default App

As you can see, we have a main route that performs the initial validation through our ProtectedRoute component, which checks if the user is authenticated.

In the nested routes, we perform additional validations based on the user’s specific roles and permissions. We also added a redirect route to which the user will be redirected if they don’t have access to the requested route.

With this simple component, our routes are protected. From here, you can add the relevant rules to your project.

As mentioned at the beginning of the tutorial, this part of route protection is one of the final steps in the entire authentication system. The permissions and/or roles to validate can come from a database, authentication token, or cookie, depending on the case.

That’s it! The app looks as follows:

See the demo here

Repo here

Conclusion

In summary, we learned how to configure private and public routes, as well as how to redirect users when they try to access unauthorized routes. Although this is a basic implementation, it provided us with a fundamental understanding of how to secure our routes using React Router.


Read more:

Want to connect with the Author?

Love connecting with friends all around the world on Twitter.


Also published here.


Written by ljaviertovar | ☕ FrontEnd engineer 👨‍💻 Indie maker ✍️ Tech writer
Published by HackerNoon on 2023/08/30