DEV Community

Cover image for A Step-by-Step Guide to Using React Router v6
Francisco Mendes
Francisco Mendes

Posted on • Edited on

A Step-by-Step Guide to Using React Router v6

In this article, we are going to use react router version 6 and we are going to create a simple react application with some essential/important elements of a web application, such as protecting routes and having unauthorized and not found pages.

Introduction

In version 6 of react router there were several things that were added and others that were changed, but they brought more flexibility when routing in a web application.

Prerequisites

Before going further, you need:

  • NPM
  • React
  • React Context
  • React Router

In addition, it is expected to have a basic knowledge of these technologies.

Getting Started

Create project setup

As a first step, let's scaffold a react app using Vite:

# npm 6.x
npm create vite@latest router-app --template react

# npm 7+, extra double-dash is needed:
npm create vite@latest router-app -- --template react
Enter fullscreen mode Exit fullscreen mode

Then, inside our project folder, we install the following dependency:

npm install react-router-dom --save
Enter fullscreen mode Exit fullscreen mode

Now in our index.html add the following link for us to use this css framework so we don't deal with classNames:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css"
    />
    <title>Vite App</title>
  </head>
  <!-- ... -->
</html>
Enter fullscreen mode Exit fullscreen mode

With our project configured and the necessary dependencies installed, we can proceed to the next step.

Create Generic Components

First let's create the Not Found page:

// @src/pages/NotFound.jsx
const NotFound = () => (
  <div>
    <h1>Not Found page</h1>
    <p>The page you tried to access doesn't exist.</p>
    <p>This is a generic route.</p>
  </div>
);

export default NotFound;
Enter fullscreen mode Exit fullscreen mode

With our Not Found page created, we can proceed to create the Unauthorized page:

// @src/pages/Unauthorized.jsx
import { Link } from "react-router-dom";

const Unauthorized = () => (
  <div>
    <h1>Unauthorized page</h1>
    <p>You don't have permission to access this page.</p>
    <Link to="/login">Go back to login.</Link>
  </div>
);

export default Unauthorized;
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, the <Link /> component of react router was used, which allows us to navigate to other pages, which in this case is to the login page.

Then we can work on our Component which we will name Layout, this component will contain two things. Our navigation bar, with the <Link /> components of the respective pages we want to navigate.

As well as the <Outlet /> component that will be responsible for rendering all the child components, which in this case will be our pages. This will allow us to share the layout between a group of pages.

// @src/components/Layout.jsx
import { Link, Outlet } from "react-router-dom";

const Layout = () => (
  <div>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/login">Login</Link>
      </li>
      <li>
        <Link to="/lounge">Lounge</Link>
      </li>
    </ul>
    <Outlet />
  </div>
);

export default Layout;
Enter fullscreen mode Exit fullscreen mode

With the generic components created, we can move on to the next step.

Create Auth Context

Our auth context will be responsible for storing data about the user's authentication and from that we will determine whether or not the user has access to certain pages.

The first step is to create the context:

// @src/context/Auth.jsx
import { createContext } from "react";

const AuthContext = createContext(null);

// ...
Enter fullscreen mode Exit fullscreen mode

Then we'll create a hook so we can use the context inside the react components:

// @src/context/Auth.jsx
import { createContext, useContext } from "react";

const AuthContext = createContext(null);

export const useAuth = () => useContext(AuthContext);

// ...
Enter fullscreen mode Exit fullscreen mode

Now we can create our authentication provider:

// @src/context/Auth.jsx
import { createContext, useContext, useState } from "react";

const AuthContext = createContext(null);

export const useAuth = () => useContext(AuthContext);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

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

// ...
Enter fullscreen mode Exit fullscreen mode

Still in our authentication context file, we can create a component that will be responsible for determining if the user can access specific routes according to their authentication status.

If he is not authenticated and wants to access a protected route, he will be redirected to the Unauthorized page. Otherwise, you can easily access the routes.

// @src/context/Auth.jsx
import { createContext, useContext, useState } from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";

const AuthContext = createContext(null);

export const useAuth = () => useContext(AuthContext);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

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

export const RequireAuth = () => {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return (
      <Navigate
        to={{ pathname: "/unauthorized", state: { from: location } }}
        replace
      />
    );
  }

  return <Outlet />;
};
Enter fullscreen mode Exit fullscreen mode

Now that we have our authentication context finished, we can move on to the next step.

Create App Pages

First of all, we need to create our main page:

// @src/pages/Home.jsx
const Home = () => {
  return (
    <div>
      <h1>Home page</h1>
      <p>This route has public access.</p>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Then we can create our login page, where the user needs to enter a username so that he can be logged into our application. Once the submission is made, the user will be redirected to a protected route.

// @src/pages/Login.jsx
import { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";

import { useAuth } from "../context/Auth";

const Login = () => {
  const [username, setUsername] = useState("");
  const { setUser } = useAuth();
  const navigate = useNavigate();

  const login = useCallback(
    (e) => {
      e.preventDefault();
      setUser({ username });
      navigate("/lounge");
    },
    [setUser, username]
  );

  return (
    <div>
      <h1>Login page</h1>
      <p>This route has public access.</p>
      <form onSubmit={login}>
        <input
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="Type username..."
        />
        <button type="submit">Login</button>
      </form>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

With the Login page done, we need to create our protected route. And still on this page we are going to create a function so that the user has the option to log out.

// @src/pages/Lounge.jsx
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";

import { useAuth } from "../context/Auth";

const Lounge = () => {
  const { user, setUser } = useAuth();
  const navigate = useNavigate();

  const logout = useCallback(
    (e) => {
      e.preventDefault();
      setUser(null);
      navigate("/");
    },
    [setUser]
  );

  return (
    <div>
      <h1>Lounge page</h1>
      <p>
        Hello <strong>{user?.username}</strong>!
      </p>
      <p>Looks like you have access to this private route!</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
};

export default Lounge;
Enter fullscreen mode Exit fullscreen mode

With our application pages created, we can move on to the last step.

Define application routes

Before we start, we need to import all the necessary components:

// @src/App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";

import { AuthProvider, RequireAuth } from "./context/Auth";
import Layout from "./components/Layout";
import HomePage from "./pages/Home";
import LoginPage from "./pages/Login";
import NotFoundPage from "./pages/NotFound";
import LoungePage from "./pages/Lounge";
import UnauthorizedPage from "./pages/Unauthorized";

// ...
Enter fullscreen mode Exit fullscreen mode

Next we'll put our AuthProvider as root component and then we'll put the <BrowserRouter /> component and the react router's <Routes /> as child components.

// @src/App.jsx

// Hidden for simplicity

const App = () => {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          {/* ---------- */}
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Next we will define the Layout of our page using our <Layout /> component.

// @src/App.jsx

// Hidden for simplicity

const App = () => {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            {/* ---------- */}
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Then we can add the pages that can be accessed by the user without being authenticated (including pages related to authorization and not found):

// @src/App.jsx

// Hidden for simplicity

const App = () => {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            <Route path="/" element={<HomePage />} />
            <Route path="/login" element={<LoginPage />} />
            <Route path="*" element={<NotFoundPage />} />
            <Route path="/unauthorized" element={<UnauthorizedPage />} />
            {/* ---------- */}
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Last but not least, we can now add our protected pages together with the component responsible for determining if the user can access these routes:

// @src/App.jsx

// Hidden for simplicity

const App = () => {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            <Route path="/" element={<HomePage />} />
            <Route path="/login" element={<LoginPage />} />
            <Route path="*" element={<NotFoundPage />} />
            <Route path="/unauthorized" element={<UnauthorizedPage />} />
            <Route element={<RequireAuth />}>
              <Route path="/lounge" element={<LoungePage />} />
            </Route>
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

With everything set up, we can now do a little review.

What to expect?

If the user is not logged in, it is expected that he can only access the main and login page. As soon as he tries to access the lounge page, which is protected, he should be redirected to the unauthorized page. Also, if the user tries to access a page that does not exist in the application, the not found page must be rendered.

On the other hand, if the user is logged in, he/she can access all pages of the application, however the user cannot be redirected to the unauthorized page, as he/she is currently logged in to our app.

The result should be similar to the following:

image

If you want to have access to the source code of this example, you can always click on this link.

Hope you enjoyed this tutorial, stay tuned for more.

Top comments (2)

Collapse
 
danielshokri profile image
Daniel Shokri

Thank you!
but what if i dont want the login page to be effected from the layout, how would u solve it?

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

In that case, you can define the Login route separately, defining the path and the element.