Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

Complete guide to authentication with React Router v6

6 min read 1815

React Logo

React Router v6 is a popular and powerful routing library for React applications. It provides a declarative, component-based approach to routing and handles the common tasks of dealing with URL params, redirects, and loading data.

React Router provides one of the most intuitive APIs available and enables lazy loading and SEO-friendly server-side rendering. This latest version of React Router introduced many new concepts, like <Outlet /> and layout routes, but the documentation is still sparse.

This tutorial will demonstrate how to create protected routes and add authentication with React Router v6.

So, fire up your favorite text editor, and let’s dive in!

Jump ahead:

Getting started

Open up the terminal and create a new React project by running the following command:

> npx create-react-app ReactRouterAuthDemo
> cd ReactRouterAuthDemo

Next, install React Router as a dependency in the React app:

> npm install react-router-dom

Once the React Router dependency is installed, we’ll need to edit the src/index.js file.

Import BrowserRouter from react-router-dom and then wrap the <App /> component with <BrowserRouter />, like so:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
);

Now, we’re all set up to use React Router components and hooks from anywhere in our app.



Let’s replace the boilerplate code from the App.js file with some routes.

Basic routing

React Router provides the <Routes /> and <Route /> components that enable us to render components based on their current location.

import { Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/Login";
import { HomePage } from "./pages/Home";
import "./styles.css";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/login" element={<LoginPage />} />
    </Routes>
  );
}

Basic routing with <Route />

<Route /> provides the mapping between paths on the app and different React components. For example, to render the LoginPage component when someone navigates to /login, we just need to provide the <Route />, like so:

<Route path="/login" element={<LoginPage />} />

The <Route /> component can be thought of like an if statement; it will act upon a URL location with its element only if it matches the specified path.

Basic routing with <Routes />

The <Routes /> component is an alternative to the <Switch /> component from React Router v5.

To use <Routes />, we’ll first create Login.jsx and Home.jsx files inside the pages directory with the following content:

// Login.jsx
export const LoginPage = () => (
  <div>
    <h1>This is the Login Page</h1>
  </div>
);

// Home.jsx
export const HomePage = () => (
  <div>
    <h1>This is the Home Page</h1>
  </div>
);

Next, we’ll run this command to start the app:

> npm run start

On the browser, we see the HomePage component by default. If we go the /login route we’ll see the LoginPage component render on the screen.

Alternatively, we can use a plain JavaScript object to represent the routes in our app using the useRoutes hook. This is a functional approach for defining routes and works in the same manner as the combination of <Routes /> and <Route /> components.

import { useRoutes } from "react-router-dom";
// ...

export default function App() {
  const routes = useRoutes([
    {
      path: "/",
      element: <HomePage />
    },
    {
      path: "/login",
      element: <LoginPage />
    }
  ]);
  return routes;
}

Now that the basic setup is completed, let’s look at how we can create protected routes so that unauthenticated users cannot access certain content in our application.

Creating protected routes

Before creating the protected route, let’s create a custom hook that will handle the authenticated user’s state using the Context API and useContext hook.

import { createContext, useContext, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useLocalStorage } from "./useLocalStorage";
const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useLocalStorage("user", null);
  const navigate = useNavigate();

  // call this function when you want to authenticate the user
  const login = async (data) => {
    setUser(data);
    navigate("/profile");
  };

  // call this function to sign out logged in user
  const logout = () => {
    setUser(null);
    navigate("/", { replace: true });
  };

  const value = useMemo(
    () => ({
      user,
      login,
      logout
    }),
    [user]
  );
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = () => {
  return useContext(AuthContext);
};

With the useAuth hook, we are exposing the user’s state and a couple of methods for user login and logout. When the user logs out, we redirect them to the home page using React Router’s useNavigate hook.

To persist the user’s state even on page refresh, we‘ll use the useLocalStorage hook which will sync the state value in the browser’s local storage.

import { useState } from "react";

export const useLocalStorage = (keyName, defaultValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const value = window.localStorage.getItem(keyName);
      if (value) {
        return JSON.parse(value);
      } else {
        window.localStorage.setItem(keyName, JSON.stringify(defaultValue));
        return defaultValue;
      }
    } catch (err) {
      return defaultValue;
    }
  });
  const setValue = (newValue) => {
    try {
      window.localStorage.setItem(keyName, JSON.stringify(newValue));
    } catch (err) {}
    setStoredValue(newValue);
  };
  return [storedValue, setValue];
};

The <ProtectedRoute /> component will simply check the current user state from the useAuth hook and then redirect to the Home screen if the user is not authenticated.

import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const ProtectedRoute = ({ children }) => {
  const { user } = useAuth();
  if (!user) {
    // user is not authenticated
    return <Navigate to="/" />;
  }
  return children;
};

To redirect the user, we use the <Navigate /> component. The <Navigate /> component changes the current location when it is rendered by the parent component. It uses the useNavigate hook internally.

In the App.js file, we can wrap the page component with the <ProtectedRoute /> component. For example below, we are wrapping the <SettingsPage /> and <ProfilePage /> components with <ProtectedRoute />. Now when unauthenticated users try to access /profile or /settings path they will be redirected to the home page.

import { Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/Login";
import { HomePage } from "./pages/Home";
import { SignUpPage } from "./pages/SignUp";
import { ProfilePage } from "./pages/Profile";
import { SettingsPage } from "./pages/Settings";
import { ProtectedRoute } from "./components/ProtectedRoute";


export default function App() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/login" element={<LoginPage />} />
      <Route path="/register" element={<SignUpPage />} />
      <Route
        path="/profile"
        element={
          <ProtectedRoute>
            <ProfilePage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/settings"
        element={
          <ProtectedRoute>
            <SettingsPage />
          </ProtectedRoute>
        }
      />
    </Routes>
  );
}

Well, the above approach works fine if there are a limited number of protected routes, but if there are multiple such routes we would have to wrap each one, which is tedious.

Instead, we can use the React Router v6 nested route feature to wrap all the protected routes in a single layout.


More great articles from LogRocket:


Using nested routes and <Outlet />

One of the most powerful features in React Router v6 is nested routes. This feature allows us to have a route that contains other child routes. The majority of our layouts are coupled to segments on the URL, and React Router supports this fully.

For example, we can add a parent <Route /> component to the <HomePage /> and <LoginPage /> routes, like so:

import { ProtectedLayout } from "./components/ProtectedLayout";
import { HomeLayout } from "./components/HomeLayout";
// ...

export default function App() {
  return (
    <Routes>
      <Route element={<HomeLayout />}>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
      </Route>

      <Route path="/dashboard" element={<ProtectedLayout />}>
        <Route path="profile" element={<ProfilePage />} />
        <Route path="settings" element={<SettingsPage />} />
      </Route>
    </Routes>
  );
}

The parent <Route /> component can also have a path and is responsible for rendering the child <Route /> component on the screen.

When the user navigates to /dashboard/profile the router will render the <ProfilePage />. In order for this to occur, the parent route element must have an <Outlet /> component to render the child elements. The Outlet component enables nested UI to be visible when child routes are rendered.

The parent route element can also have additional common business logic and user interface. For example, in the <ProtectedLayout /> component we have included the private route logic and also a common navigation bar that will be visible when the child routes are rendered.

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const ProtectedLayout = () => {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/" />;
  }

  return (
    <div>
      <nav>
        <Link to="/settings">Settings</Link>
        <Link to="/profile">Profile</Link>
      </nav>
      <Outlet />
    </div>
  )
};

Instead of the <Outlet /> component, we can also opt to use the useOutlet hook which serves the same purpose:

import { Link, Navigate, useOutlet } from "react-router-dom";
// ...

export const ProtectedLayout = () => {
  const { user } = useAuth();
  const outlet = useOutlet();

  if (!user) {
    return <Navigate to="/" />;
  }

  return (
    <div>
      <nav>
        <Link to="/settings">Settings</Link>
        <Link to="/profile">Profile</Link>
      </nav>
      {outlet}
    </div>
  );
};

Similar to protected routes, we do not want authenticated users to access the /login path. Let’s handle that in the <HomeLayout /> component:

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const HomeLayout = () => {
  const { user } = useAuth();

  if (user) {
    return <Navigate to="/dashboard/profile" />;
  }

  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/login">Login</Link>
      </nav>
      <Outlet />
    </div>
  )
};

You can check out the complete code and demo in this CodeSandbox.

Conclusion

It’s worth investing some time to better understand how React Router v6 works, particularly for the common use case of user authentication.

React Router v6 is a huge improvement over previous versions. It’s fast, stable, and reliable. In addition to being easier to work with, it has many new features like <Outlets /> and an improved <Route /> component that have greatly simplified routing in React apps.

I hope you found this guide helpful and now have a better understanding of how to handle user authentication with React Router v6.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

2 Replies to “Complete guide to authentication with React Router v6”

  1. In the example where login path is removed after login, the home page is no longer accessible. To reproduce, after login, try to go to home page. It will always redirect to /dashboard/profile. I think this shouldnt be the ideal scenario.

  2. i cant use this scenario because im using a theme that used react router dom v6 and useRoutes i dont have routes and route jsx and i cant replace them

Leave a Reply