React Router v6: A Beginner’s Guide

    James Hibbard
    James Hibbard
    Share

    React Router is the de facto standard routing library for React. When you need to navigate through a React application with multiple views, you’ll need a router to manage the URLs. React Router takes care of that, keeping your application UI and the URL in sync.

    This tutorial introduces you to React Router v6 and a whole lot of things you can do with it.

    Table of Contents

    Key Takeaways

    • Essential Guide to React Router v6: This comprehensive tutorial offers an in-depth exploration of React Router v6, covering the basics of setting up and using the library for efficient navigation and URL management in React applications.

    • Navigating and Rendering Routes: Learn how to create dynamic and nested routes using essential React Router components like <Routes>, <Route>, and <Link>, along with the implementation of path parameters for more flexible routing solutions.

    • Advanced Routing Techniques: The tutorial delves into advanced concepts such as protected routes, programmatic navigation with useNavigate, and the latest features introduced in React Router v6.4, enhancing your understanding of routing in modern React applications

    Introduction

    React is a popular JavaScript library for building interactive web applications that can deliver dynamic content. Such applications might have multiple views (aka pages), but unlike conventional multi-page apps, navigating through these views shouldn’t result in the entire page being reloaded. Instead, views are rendered inline within the current page.

    The end user, who’s accustomed to multi-page apps, expects the following features to be present:

    • Each view should have a URL that uniquely specifies that view. This is so that the user can bookmark the URL for reference at a later time — for example, www.example.com/products.
    • The browser’s back and forward button should work as expected.
    • Dynamically generated nested views should preferably have a URL of their own too — such as example.com/products/shoes/101, where 101 is the product ID.

    Routing is the process of keeping the browser URL in sync with what’s being rendered on the page. React Router lets you handle routing declaratively. The declarative routing approach allows you to control the data flow in your application, by saying “the route should look like this”:

    <Route path="/about" element={<About />} />
    

    You can place your <Route> component anywhere you want your route to be rendered. Since <Route>, <Link> and all the other APIs that we’ll be dealing with are just components, you can easily get up and running with routing in React.

    Note: there’s a common misconception that React Router is an official routing solution developed by Facebook. In reality, it’s a third-party library that’s developed and maintained by Remix Software.

    Overview

    This tutorial is divided into different sections. First, we’ll set up React and React Router using npm. Then we’ll jump right into some basics. You’ll find different code demonstrations of React Router in action. The examples covered in this tutorial include:

    • basic navigational routing
    • nested routing
    • nested routing with path parameters
    • protected routing

    All the concepts connected with building these routes will be discussed along the way.

    The entire code for the project is available on this GitHub repo.

    Let’s get started!

    Setting up React Router

    To follow along with this tutorial, you’ll need a recent version of Node installed on your PC. If this isn’t the case, then head over to the Node home page and download the correct binaries for your system. Alternatively, you might consider using a version manager to install Node. We have a tutorial on using a version manager here.

    Node comes bundled with npm, a package manager for JavaScript, with which we’re going to install some of the libraries we’ll be using. You can learn more about using npm here.

    You can check that both are installed correctly by issuing the following commands from the command line:

    node -v
    > 20.9.0
    
    npm -v
    > 10.1.0
    

    With that done, let’s start off by creating a new React project with the Create React App tool. You can either install this globally, or use npx, like so:

    npx create-react-app react-router-demo
    

    When this has finished, change into the newly created directory:

    cd react-router-demo
    

    The library comprises three packages: react-router, react-router-dom, and react-router-native. The core package for the router is react-router, whereas the other two are environment specific. You should use react-router-dom if you’re building a web application, and react-router-native if you’re in a mobile app development environment using React Native.

    Use npm to install react-router-dom package:

    npm install react-router-dom
    

    Then start the development server with this:

    npm run start
    

    Congratulations! You now have a working React app with React Router installed. You can view the app running at http://localhost:3000/.

    React Router Basics

    Now let’s familiarize ourselves with a basic setup. To do this, we’ll make an app with three separate views: Home, Category and Products.

    The Router Component

    The first thing we’ll need to do is to wrap our <App> component in a <Router> component (provided by React Router). There are several kinds of router available, but in our case, there are two which merit consideration:

    The primary difference between them is evident in the URLs they create:

    // <BrowserRouter>
    https://example.com/about
    
    // <HashRouter>
    https://example.com/#/about
    

    The <BrowserRouter> is commonly used as it leverages the HTML5 History API to synchronize your UI with the URL, offering a cleaner URL structure without hash fragments. On the other hand, the <HashRouter> utilizes the hash portion of the URL (window.location.hash) to manage routing, which can be beneficial in environments where server configuration is not possible or when supporting legacy browsers lacking HTML5 History API support. You can read more about the differences here.

    Note also that four new routers, which support various new data APIs, were introduced in a recent version of React Router (v6.4). In this tutorial, we’ll focus on the traditional routers, as they are robust, well documented and used in a myriad of projects across the internet. We will, however, dive into what’s new in v6.4 in a later section.

    So, let’s import the <BrowserRouter> component and wrap it around the <App> component. Change index.js to look like this:

    // src/index.js
    
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    import { BrowserRouter } from 'react-router-dom';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </React.StrictMode>
    );
    

    This code creates a history instance for our entire <App> component. Let’s look at what that means.

    A Little Bit of History

    The history library lets you easily manage session history anywhere JavaScript runs. A history object abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, and persist state between sessions. — remix-run

    Each <Router> component creates a history object that keeps track of the current and previous locations in a stack. When the current location changes, the view is re-rendered and you get a sense of navigation.

    How does the current location change? In React Router v6, the useNavigate hook provides a navigate function that can be used for this purpose. The navigate function is invoked when you click on a <Link> component, and it can also be used to replace the current location by passing an options object with a replace: true property.

    Other methods — such as navigate(-1) for going back and navigate(1) for going forward — are used to navigate through the history stack by going back or forward a page.

    Apps don’t need to create their own history objects; this task is managed by the <Router> component. In a nutshell, it creates a history object, subscribes to changes in the stack, and modifies its state when the URL changes. This triggers a re-render of the app, ensuring the appropriate UI is displayed.

    Moving on, we have Links and Routes.

    Link and Route Components

    The <Route> component is the most important component in React Router. It renders some UI if the location matches the current route path. Ideally, a <Route> component should have a prop named path, and if the path name matches the current location, it gets rendered.

    The <Link> component, on the other hand, is used to navigate between pages. It’s comparable to the HTML anchor element. However, using anchor links would result in a full page refresh, which we don’t want. So instead, we can use <Link> to navigate to a particular URL and have the view re-rendered without a refresh.

    Now we’ve covered everything you need to make our app work. Delete all files apart from index.js and App.js from the project’s src folder, then update App.js as follows:

    import { Link, Route, Routes } from 'react-router-dom';
    
    const Home = () => (
      <div>
        <h2>Home</h2>
        <p>Welcome to our homepage!</p>
      </div>
    );
    
    const Categories = () => (
      <div>
        <h2>Categories</h2>
        <p>Browse items by category.</p>
      </div>
    );
    
    const Products = () => (
      <div>
        <h2>Products</h2>
        <p>Browse individual products.</p>
      </div>
    );
    
    export default function App() {
      return (
        <div>
          <nav>
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/categories">Categories</Link>
              </li>
              <li>
                <Link to="/products">Products</Link>
              </li>
            </ul>
          </nav>
    
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/categories" element={<Categories />} />
            <Route path="/products" element={<Products />} />
          </Routes>
        </div>
      );
    }
    

    Here, we’ve declared three components — <Home>, <Categories>, and <Products> — which represent different pages in our application. The <Routes> and <Route> components imported from React Router are utilized to define the routing logic.

    In the <App> component we have a basic navigation menu, where each item is a <Link> component from React Router. The <Link> components are used to create navigable links to different parts of the application, each associated with a specific path (/, /categories and /products respectively). Note that in a larger app, this menu could be encapsulated within a layout component to maintain a consistent structure across different views. You might also want to add some kind of active class (such as using a NavLink component) to the currently selected nav item. However, to keep things focussed, we’ll skip this here.

    Below the navigation menu, the <Routes> component is used as a container for a collection of individual <Route> components. Each <Route> component is associated with a specific path and a React component to render when the path matches the current URL. For example, when the URL is /categories, the <Categories> component is rendered.

    Note: in previous versions of React Router, / would match both / and /categories, meaning that both components were rendered. The solution to this would have been to pass the exact prop to the <Route>, ensuring that only the exact path was matched. This behavior changed in v6, so that now all paths match exactly by default. As we’ll see in the next section, if you want to match more of the URL because you have child routes, use a trailing * — such as <Route path="categories/*" ...>.

    If you’re following along, before proceeding, take a moment to click around the app and make sure everything behaves as expected.

    Nested Routing

    Top-level routes are all well and good, but before long most applications will need to be able to nest routes — for example, to display a particular product, or to edit a specific user.

    In React Router v6, routes are nested by placing <Route> components inside other <Route> components in the JSX code. This way, the nested <Route> components naturally reflect the nested structure of the URLs they represent.

    Let’s look at how we can implement this in our app. Change App.js, like so (where ... indicates that the previous code remains unchanged):

    import { Link, Route, Routes } from 'react-router-dom';
    import { Categories, Desktops, Laptops } from './Categories';
    
    const Home = () => ( ... );
    const Products = () => ( ... );
    
    export default function App() {
      return (
        <div>
          <nav>...</nav>
    
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/categories/" element={<Categories />}>
              <Route path="desktops" element={<Desktops />} />
              <Route path="laptops" element={<Laptops />} />
            </Route>
            <Route path="/products" element={<Products />} />
          </Routes>
        </div>
      );
    }
    

    As you can see, we’ve moved the <Categories> component into its own page and are now importing two further components, namely <Desktops> and <Laptops>.

    We’ve also made some changes to the <Routes> component, which we’ll look at in a moment.

    First, create a Categories.js file in the same folder as your App.js file. Then add the following code:

    // src/Categories.js
    import { Link, Outlet } from 'react-router-dom';
    
    export const Categories = () => (
      <div>
        <h2>Categories</h2>
        <p>Browse items by category.</p>
        <nav>
          <ul>
            <li>
              <Link to="desktops">Desktops</Link>
            </li>
            <li>
              <Link to="laptops">Laptops</Link>
            </li>
          </ul>
        </nav>
    
        <Outlet />
      </div>
    );
    
    export const Desktops = () => <h3>Desktop PC Page</h3>;
    export const Laptops = () => <h3>Laptops Page</h3>;
    

    Refresh your app (this should happen automatically if the dev server is running) and then click on the Categories link. You should now see two new menu points (Desktops and Laptops) and clicking on either one will display a new page inside the original Categories page.

    So what did we just do?

    In App.js we changed our /categories route to look like this:

    <Route path="/categories/" element={<Categories />}>
      <Route path="desktops" element={<Desktops />} />
      <Route path="laptops" element={<Laptops />} />
    </Route>
    

    In the updated code, the <Route> component for /categories has been modified to include two nested <Route> components within it — one for /categories/desktops and another for /categories/laptops. This change illustrates how React Router allows for composability with its routing configuration.

    By nesting <Route> components within the /categories <Route>, we’re able to create a more structured URL and UI hierarchy. This way, when a user navigates to /categories/desktops or /categories/laptops, the respective <Desktops> or <Laptops> component will be rendered within the <Categories> component, showcasing a clear parent–child relationship between the routes and components.

    Note: the path of a nested route is automatically composed by concatenating the paths of its ancestors with its own path.

    We’ve also altered our <Categories> component to include an <Outlet />:

    export const Categories = () => (
      <div>
        <h2>Categories</h2>
        ...
        <Outlet />
      </div>
    );
    

    An <Outlet> is placed in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered.

    This compositional approach makes the routing configuration more declarative and easier to understand, aligning well with React’s component-based architecture.

    Accessing Router Properties with Hooks

    In earlier versions, certain props were passed implicitly to a component. For example:

    const Home = (props) => {
      console.log(props);
      return ( <h2>Home</h2> );
    };
    

    The code above would log the following:

    {
      history: { ... },
      location: { ... },
      match: { ... }
    }
    

    In React Router version 6, the approach to passing router props has shifted to provide a more explicit and hook-based method. The router props history, location, and match are no longer passed implicitly to a component. Instead, a set of hooks are provided to access this information.

    For instance, to access the location object, you would use the useLocation hook. The useMatch hook returns match data about a route at the given path. The history object is no longer explicitly surfaced, rather the useNavigate hook will return a function that lets you navigate programmatically.

    There are many more hooks to explore and rather than list them all here, I would encourage you to check out the official documentation, where available hooks can be found in the sidebar on the left.

    Next, let’s look at one of those hooks in more detail and make our previous example more dynamic.

    Nested Dynamic Routing

    To start with, change the routes in App.js, like so:

    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/categories/" element={<Categories />}>
        <Route path="desktops" element={<Desktops />} />
        <Route path="laptops" element={<Laptops />} />
      </Route>
      <Route path="/products/*" element={<Products />} />
    </Routes>
    

    The eagle-eyed among you will spot that there’s now a trailing /* on the /products route. In React Router version 6, the /* is a way to indicate that the <Products> component can have child routes, and it’s a placeholder for any additional path segments that might follow /products in the URL. This way, when you navigate to a URL like /products/laptops, the <Products> component will still be matched and rendered, and it will be able to further process the laptops part of the path using its own nested <Route> elements.

    Next, let’s move the <Products> component into its own file:

    // src/App.js
    ...
    import Products from './Products';
    
    const Home = () => ( ... );
    
    export default function App() { ... }
    

    Finally, create a Products.js file and add the following code:

    // src/Products.js
    import { Route, Routes, Link, useParams } from 'react-router-dom';
    
    const Item = () => {
      const { name } = useParams();
    
      return (
        <div>
          <h3>{name}</h3>
          <p>Product details for the {name}</p>
        </div>
      );
    };
    
    const Products = () => (
      <div>
        <h2>Products</h2>
        <p>Browse individual products.</p>
        <nav>
          <ul>
            <li>
              <Link to="dell-optiplex-3900">Dell OptiPlex 3090</Link>
            </li>
            <li>
              <Link to="lenovo-thinkpad-x1">Lenovo ThinkPad X1</Link>
            </li>
          </ul>
        </nav>
    
        <Routes>
          <Route path=":name" element={<Item />} />
        </Routes>
      </div>
    );
    
    export default Products;
    

    Here we’ve added a <Route> to an <Item> component (declared at the top of the page). The route’s path is set to :name, which will match any path segment following its parent route and pass that segment as a parameter named name to the <Item> component.

    Inside of the <Item> component, we’re using the useParams hook. This returns an object of key/value pairs of the dynamic params from the current URL. If we were to log it to the console for the route /products/laptops, we would see:

    Object { "*": "laptops", name: "laptops" }
    

    We can then use object destructuring to grab this parameter directly, and then render it inside an <h3> tag.

    Try it out! As you’ll see, the <Item> component catches any links you declare in your nav bar and creates a page dynamically.

    You can also try adding some more menu items:

    <li>
      <Link to="cyberpowerpc-gamer-xtreme">CyberPowerPC Gamer Xtreme</Link>
    </li>
    

    Our app will take these new pages into account.

    This method of capturing dynamic segments of the URL and using them as parameters within our components allows for more flexible routing and component rendering based on the URL structure.

    Let’s build on this in the next section.

    Nested Routing with Path Parameters

    In a real-world app, a router will have to deal with data and display it dynamically. Let’s assume we have some product data returned by an API in the following format:

    const productData = [
      {
        id: 1,
        name: "Dell OptiPlex 3090",
        description:
          "The Dell OptiPlex 3090 is a compact desktop PC that offers versatile features to meet your business needs.",
        status: "Available",
      },
      {
        id: 2,
        name: "Lenovo ThinkPad X1 Carbon",
        description:
          "Designed with a sleek and durable build, the Lenovo ThinkPad X1 Carbon is a high-performance laptop ideal for on-the-go professionals.",
        status: "Out of Stock",
      },
      {
        id: 3,
        name: "CyberPowerPC Gamer Xtreme",
        description:
          "The CyberPowerPC Gamer Xtreme is a high-performance gaming desktop with powerful processing and graphics capabilities for a seamless gaming experience.",
        status: "Available",
      },
      {
        id: 4,
        name: "Apple MacBook Air",
        description:
          "The Apple MacBook Air is a lightweight and compact laptop with a high-resolution Retina display and powerful processing capabilities.",
        status: "Out of Stock",
      },
    ];
    

    Let’s also assume that we need routes for the following paths:

    • /products: this should display a list of products.
    • /products/:productId: if a product with the :productId exists, it should display the product data, and if not, it should display an error message.

    Replace the current contents of Products.js with the following (making sure to copy in the product data from above):

    import { Link, Route, Routes } from "react-router-dom";
    import Product from "./Product";
    
    const productData = [ ... ];
    
    const Products = () => {
      const linkList = productData.map((product) => {
        return (
          <li key={product.id}>
            <Link to={`${product.id}`}>{product.name}</Link>
          </li>
        );
      });
    
      return (
        <div>
          <h3>Products</h3>
          <p>Browse individual products.</p>
          <ul>{linkList}</ul>
    
          <Routes>
            <Route path=":productId" element={<Product data={productData} />} />
            <Route index element={<p>Please select a product.</p>} />
          </Routes>
        </div>
      );
    };
    
    export default Products;
    

    Inside the component, we build a list of <Link> components using the id property from each of our products. We store this in a linkList variable, before rendering it out to the page.

    Next come two <Route> components. The first has a path prop with the value :productId, which (as we saw previously) is a route parameter. This allows us to capture and use the value from the URL at this segment as productId. The element prop of this <Route> component is set to render a <Product> component, passing it the productData array as a prop. Whenever the URL matches the pattern, this <Product> component will be rendered, with the respective productId captured from the URL.

    The second <Route> component uses an index prop to render the text “Please select a product” whenever the URL matches the base path exactly. The index prop signifies that this route is the base or “index” route within this <Routes> setup. So, when the URL matches the base path — that is, /products — this message will be displayed.

    Now, here’s the code for the <Product> component we referenced above. You’ll need to create this file at src/Product.js:

    import { useParams } from 'react-router-dom';
    
    const Product = ({ data }) => {
      const { productId } = useParams();
      const product = data.find((p) => p.id === Number(productId));
    
      return (
        <div>
          {product ? (
            <div>
              <h3> {product.name} </h3>
              <p>{product.description}</p>
              <hr />
              <h4>{product.status}</h4>
            </div>
          ) : (
            <h2>Sorry. Product doesn't exist.</h2>
          )}
        </div>
      );
    };
    
    export default Product;
    

    Here we’re making use of the useParams hook to access the dynamic parts of the URL path as key/value pairs. Again, we are using destructuring to grab the data we’re interested in (the productId).

    The find method is being used on the data array to search for and return the first element whose id property matches the productId retrieved from the URL parameters.

    Now when you visit the application in the browser and select Products, you’ll see a submenu rendered, which in turn displays the product data.

    Before moving on, have a play around with the demo. Assure yourself that everything works and that you understand what’s happening in the code.

    Protecting Routes

    A common requirement for many modern web apps is to ensure that only logged-in users can access certain parts of the site. In this next section, we’ll look at how to implement a protected route, so that if someone tries to access /admin, they’ll be required to log in.

    However, there are a couple of aspects of React Router that we need to cover first.

    In version 6, programmatically redirecting to a new location is achieved through the useNavigate hook. This hook provides a function that can be used to programmatically navigate to a different route. It can accept an object as a second parameter, used to specify various options. For example:

    const navigate = useNavigate();
    navigate('/login', {
      state: { from: location },
      replace: true
    });
    

    This will redirect the user to /login, passing along a location value to store in history state, which we can then access on the destination route via a useLocation hook. Specifying replace: true will also replace the current entry in the history stack rather than adding a new one. This mimics the behavior of the now defunct <Redirect> component in v5.

    To summarize: if someone tries to access the /admin route while logged out, they’ll be redirected to the /login route. The information about the current location is passed via the state prop, so that if the authentication is successful, the user can be redirected back to the page they were originally trying to access.

    Custom Routes

    The next thing we need to look at are custom routes. A custom route in React Router is a user-defined component that allows for additional functionality or behaviors during the routing process. It can encapsulate specific routing logic, such as authentication checks, and render different components or perform actions based on certain conditions.

    Create a new file PrivateRoute.js in the src directory and add the following content:

    import { useEffect } from 'react';
    import { useNavigate, useLocation } from 'react-router-dom';
    import { fakeAuth } from './Login';
    
    const PrivateRoute = ({ children }) => {
      const navigate = useNavigate();
      const location = useLocation();
    
      useEffect(() => {
        if (!fakeAuth.isAuthenticated) {
          navigate('/login', {
            state: { from: location },
            replace: true,
          });
        }
      }, [navigate, location]);
    
      return fakeAuth.isAuthenticated ? children : null;
    };
    
    export default PrivateRoute;
    

    There are several things going on here. First of all, we import something called fakeAuth, which exposes an isAuthenticated property. We’ll look at this in more detail soon, but for now it’s sufficient to know that this is what we’ll use to determine the user’s logged-in status.

    The component accepts a children prop. This will be the protected content that the <PrivateRoute> component is wrapped around when it’s called. For example:

    <PrivateRoute>
      <Admin /> <-- children
    </PrivateRoute>
    

    In the component body, the useNavigate and useLocation hooks are used to obtain the navigate function and the current location object respectively. If the user isn’t authenticated, as checked by !fakeAuth.isAuthenticated, the navigate function is called to redirect the user to the /login route, as described in the previous section.

    The component’s return statement checks the authentication status again. If the user is authenticated, its child components are rendered. If the user isn’t authenticated, null is returned, rendering nothing.

    Note also that we’re wrapping the call to navigate in React’s useEffect hook. This is because the navigate function should not be called directly inside the component body, as it causes a state update during rendering. Wrapping it inside a useEffect hook ensures that it’s called after the component is rendered.

    Important Security Notice

    In a real-world app, you need to validate any request for a protected resource on your server. Let me say that again…

    In a real-world app, you need to validate any request for a protected resource on your server.

    This is because anything that runs on the client can potentially be reverse engineered and tampered with. For example, in the above code one can just open React’s dev tools and change the value of isAuthenticated to true, thus gaining access to the protected area.

    Authentication in a React app is worthy of a tutorial of its own, but one way to implement it would be using JSON Web Tokens. For example, you could have an endpoint on your server which accepts a username and password combination. When it receives these (via Ajax), it checks to see if the credentials are valid. If so, it responds with a JWT, which the React app saves (for example, in sessionStorage), and if not, it sends a 401 Unauthorized response back to the client.

    Assuming a successful login, the client would then send the JWT as a header along with any request for a protected resource. This would then be validated by the server before it sent a response.

    When storing passwords, the server would not store them in plaintext. Rather, it would encrypt them — for example, using bcryptjs.

    Implementing the Protected Route

    Now let’s implement our protected route. Alter App.js like so:

    import { Link, Route, Routes } from 'react-router-dom';
    import { Categories, Desktops, Laptops } from './Categories';
    import Products from './Products';
    import Login from './Login';
    import PrivateRoute from './PrivateRoute';
    
    const Home = () => (
      <div>
        <h2>Home</h2>
        <p>Welcome to our homepage!</p>
      </div>
    );
    
    const Admin = () => (
      <div>
        <h2>Welcome admin!</h2>
      </div>
    );
    
    export default function App() {
      return (
        <div>
          <nav>
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/categories">Categories</Link>
              </li>
              <li>
                <Link to="/products">Products</Link>
              </li>
              <li>
                <Link to="/admin">Admin area</Link>
              </li>
            </ul>
          </nav>
    
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/categories/" element={<Categories />}>
              <Route path="desktops" element={<Desktops />} />
              <Route path="laptops" element={<Laptops />} />
            </Route>
            <Route path="/products/*" element={<Products />} />
            <Route path="/login" element={<Login />} />
            <Route
              path="/admin"
              element={
                <PrivateRoute>
                  <Admin />
                </PrivateRoute>
              }
            />
          </Routes>
        </div>
      );
    }
    

    As you can see, we’ve added an <Admin> component to the top of the file, and we’re including our <PrivateRoute> within the <Routes> component. As mentioned previously, this custom route renders the <Admin> component if the user is logged in. Otherwise, the user is redirected to /login.

    Finally, create Login.js and add the following code:

    // src/Login.js
    
    import { useState, useEffect } from 'react';
    import { useNavigate, useLocation } from 'react-router-dom';
    
    export default function Login() {
      const navigate = useNavigate();
      const { state } = useLocation();
      const from = state?.from || { pathname: '/' };
      const [redirectToReferrer, setRedirectToReferrer] = useState(false);
    
      const login = () => {
        fakeAuth.authenticate(() => {
          setRedirectToReferrer(true);
        });
      };
    
      useEffect(() => {
        if (redirectToReferrer) {
          navigate(from.pathname, { replace: true });
        }
      }, [redirectToReferrer, navigate, from.pathname]);
    
      return (
        <div>
          <p>You must log in to view the page at {from.pathname}</p>
          <button onClick={login}>Log in</button>
        </div>
      );
    }
    
    /* A fake authentication function */
    export const fakeAuth = {
      isAuthenticated: false,
      authenticate(cb) {
        this.isAuthenticated = true;
        setTimeout(cb, 100);
      },
    };
    

    In the code above, we’re attempting to get a value for the URL the user was trying to access before being asked to log in. If this isn’t present, we set it to { pathname: "/" }.

    We then use React’s useState hook to initialize a redirectToReferrer property to false. Depending on the value of this property, the user is either redirected to where they were going (that is, the user is logged in), or the user is presented with a button to log them in.

    Once the button is clicked, the fakeAuth.authenticate method is executed, which sets fakeAuth.isAuthenticated to true and (in a callback function) updates the value of redirectToReferrer to true. This causes the component to re-render and the user to be redirected.

    Working Demo

    Let’s fit the puzzle pieces together, shall we? Here’s the final demo of the application we built using React router.

    React Router Version 6.4

    Before we finish up, we should mention the release of React Router v6.4. Despite looking like an inconspicuous point release, this version introduced some groundbreaking new features. For example, it now includes the data loading and mutation APIs from Remix, which introduce a whole new paradigm for keeping the UI in sync with your data.

    As of version 6.4, you can define a loader function for each route, which is responsible for fetching the data needed for that route. Inside your component, you use the useLoaderData hook to access the data that was loaded by your loader function. When a user navigates to a route, React Router automatically calls the associated loader function, fetches the data, and passes the data to the component via the useLoaderData hook, without a useEffect in sight. This promotes a pattern where data fetching is tied directly to routing.

    There is also a new <Form> component which prevents the browser from sending the request to the server and sends it to your route’s action instead. React Router then automatically revalidates the data on the page after the action finishes, which means all of your useLoaderData hooks update and the UI stays in sync with your data automatically.

    To use these new APIS, you’ll need to use the new <RouterProvider /> component. This takes a router prop which is created using the new createBrowserRouter function.

    Discussing all of these changes in detail is outside the scope of this article, but if you’re keen to find out more, I would encourage you to follow along with the official React Router tutorial.

    Summary

    As you’ve seen in this article, React Router is a powerful library that complements React for building better, declarative routing in your React apps. At the time of writing, the current version of React Router is v6.18 and the library has undergone substantial change since v5. This is in part due to the influence of Remix, a full-stack web framework written by the same authors.

    In this tutorial, we learned:

    • how to set up and install React Router
    • the basics of routing and some essential components such as <Routes>, <Route> and <Link>
    • how to create a minimal router for navigation and nested routes
    • how to build dynamic routes with path parameters
    • how to work with React Router’s hooks and its newer route rendering pattern

    Finally, we learned some advanced routing techniques while creating the final demo for protected routes.

    FAQs About React Router v6

    How Does Routing Work in React Router v6?

    This version introduces a new routing syntax using the <Routes> and <Route> components. The <Routes> component wraps around individual <Route> components, which specify the path and the element to render when the path matches the URL.

    How Do I Set up Nested Routes?

    Nested routes are created by placing <Route> components inside other <Route> components in the JSX code. This way, the nested <Route> components naturally reflect the nested structure of the URLs they represent.

    How Do I Redirect Users to Another Page?

    You can use the useNavigate hook to programmatically navigate users to another page. For instance, const navigate = useNavigate(); and then navigate('/path'); to redirect to the desired path.

    How Do I Pass Props to Components?

    In v6, you can pass props to components by including them in the element prop of a <Route> component, like so: <Route path="/path" element={<Component prop={value} />} />.

    How Do I Access Url Parameters in React Router v6?

    URL parameters can be accessed using the useParams hook. For example, if the route is defined as <Route path=":id" element={<Component />} />, you can use const { id } = useParams(); to access the id parameter inside <Component />.

    What’s New in React Router v6.4?

    Version 6.4 introduces many new features inspired by Remix, such data loaders and createBrowserRouter, aiming to improve data fetching and submission. You can find an exhaustive list of new features here.

    How Do I Migrate from React Router v5 to v6?

    Migrating involves updating your route configurations to the new <Routes> and <Route> components syntax, updating hooks and other API methods to their v6 counterparts, and addressing any breaking changes in your application’s routing logic. You can find an official guide here.