Mastering React Router: A Comprehensive Guide

Mastering React Router: A Comprehensive Guide

Step-by-Step Tutorial with Code Examples for Seamless Navigation in React

·

23 min read

React Router is probably the most popular routing module, however some of its more intricate capabilities might be difficult to understand. That's why in this article, I'll go over everything you need to know about React Router so you can use even the most complicated features with ease. This article will be divided into four sections; The basics, advanced route definitions, navigation, and in-depth router information.

React Router Basics

Before we get into the advanced capabilities of React Router, let's go over the basics. To use React Router on the web, first install it by running npm i react-router-dom. This library just installs the DOM version of React Router. If you're using React Native, install react-router-native instead. Aside from this minor variation, the libraries function essentially identically. In this article, the main focus will be on react-router-dom.

Once you've installed this library, you'll need to perform three things to use React Router.

  1. Router Configuration

  2. Routes Definition

  3. Navigation Handling

Router Configuration

Setting up your router is by far the easiest step. All you have to do is import the required router (BrowserRouter for web and NativeRouter for mobile) and wrap your entire application with it.

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>
)

In general, you will import your router into your application's index.js page, where it will enclose your App component. The router functions similarly to a React context, providing the required information to your application so that you may route and use all of React Router's custom hooks.

Routes Definition

import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books" element={<BookList />} />
    </Routes>
  )
}

Defining routes is as simple as creating a single path component for each path in your application and combining them into a single Routes component. When your URL changes, React Router will look at the routes declared in your Routes component and render the content in the element prop of the Route with the path that matches the URL. If our URL was /books, the BookList component would be shown.

The wonderful thing about React Router is that when you switch pages, it simply refreshes the content inside your Routes component. The remainder of the content on your page will remain the same, which improves performance and user experience.

Navigation Handling

The last step in React Router is to handle navigation. Normally, in an application, you would navigate with anchor tags, but React Router handles navigation with its own unique Link component. This Link component is simply a wrapper around an anchor tag that ensures all routing and conditional re-rendering are handled correctly, allowing you to utilize it in the same way you would a regular anchor tag.

import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/books">Books</Link>
          </li>
        </ul>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}

In our example, we included two links to the home and books pages. You'll also notice that we used the to prop to set the URL rather than the href prop, which is often used with anchor tags. This is the single distinction between the Link component and an anchor tag, and it is important to understand because it is simple to unintentionally use the href prop instead of the to prop.

Another thing to note about our new code is that the nav we're rendering at the top of our page is outside of our Routes component, so when we change pages, this nav part will not be re-rendered as only the content in the Routes component will change when the URL changes.

Advanced Route Definitions

This is where React Router becomes really fascinating. Routing allows you to create more sophisticated routes that are easier to read and more functional overall. This can be accomplished using five main ways.

  1. Dynamic Routing

  2. Routing Priority

  3. Nested Routes

  4. Multiple Routes

  5. useRoutes Hook

Dynamic Routing

This is the simplest and most used advanced feature in React Router. In our example, suppose we want to render a component for each book in our application. We could hardcode each of those routes, but if we have hundreds of books or allow users to create books, it will be impossible to hardcode all of them. Instead, we need a dynamic path.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
</Routes>

The final route in the above example is a dynamic route with a dynamic parameter :id. Defining dynamic routes with React Router is as simple as putting a colon in front of whatever you want the route's dynamic component to be. In our scenario, the dynamic route will match any URL beginning with /book and ending with some value. For example, /books/1, /books/bookName, and /books/literally-anything will all work with our dynamic route.

When dealing with a dynamic route like this, you almost always want to access the dynamic value in your own component, which is where the useParams hook comes in.

import { useParams } from "react-router-dom"

export function Book() {
  const { id } = useParams()

  return <h1>Book {id}</h1>
}

The useParams hook accepts no parameters and returns an object whose keys match the dynamic parameters in your route. In our example, our dynamic parameter is :id, therefore the useParams hook will return an object with the key id and the value of that key, which is the real id in our URL. For example, if our URL was /books/3, the page would display Book 3.

Routing Priority

When we were dealing with hard coded routes, it was quite simple to determine which route would be shown, but dealing with dynamic routes can be more difficult. Take these routes as an example.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
</Routes>

Which route would be appropriate for the URL /books/new? Technically, we have two matching routes. Both /books/:id and /books/new will match since the dynamic route will presume that new is the :id element of the URL, therefore React Router requires an additional method to identify which route to render.

In previous versions of React Router, whichever route was defined first was rendered, so in our example, the /books/:id route was rendered, which was obviously not what we wanted. Fortunately, version 6 of React Router changed this, and now React Router uses an algorithm to determine which route is most likely the one you want. In this example, we plainly want to render the /books/new route, thus React Router will select that for us. This algorithm operates similarly to CSS specificity in that it attempts to determine the route that fits our URL is the most specific (has the fewest dynamic elements) and selects that route.

While we're talking about routing priority, I'd want to discuss how to design a route that fits anything.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
  <Route path="*" element={<NotFound />} />
</Routes>

A * matches anything, making it ideal for things like 404 pages. A route with a is likewise less particular than anything else, thus you will never unintentionally match a * route when another route would have matched as well.

Nested Routes

Finally, we get to my favorite element of React Router: how they manage route nesting. In the above example, we have three routes beginning with /books, which we may nest inside of each other to clean up our routes.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books">
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

This nesting is rather straightforward to do. All you have to do is create a parent Route with the path prop set to the shared path for each of your child Route components. Then, inside the parent Route, you can add all of the child Route components. The sole difference is that the child Route components' path props no longer include the shared /books route. In addition, the route for /books has been updated with a Route component that contains an index prop rather than a path prop. This simply means that the index Route's path is identical to that of the parent Route.

Now, if this was all you could do with nested routes, it would be just minimally beneficial; however, the full strength of nested routes lies in how it manages shared layout.

Shared Layouts

Assume we want to display a navigation section with links to each book as well as the new book form on any of our book pages. To achieve this properly, we would need to create a shared component to hold this navigation and then import it into each book-related component. This is a bit of a nuisance, so React Router came up with its own solution. If you send an element prop to a parent route, the component will be rendered for each and every child route, allowing you to easily place a shared nav or other shared components on each child page.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/books/1">Book 1</Link>
          </li>
          <li>
            <Link to="/books/2">Book 2</Link>
          </li>
          <li>
            <Link to="/books/new">New Book</Link>
          </li>
        </ul>
      </nav>

      <Outlet />
    </>
  )
}

Our new code will operate by rendering the BooksLayout component, which provides our shared navigation, anytime we match a route within the /book parent route. The corresponding child Route will be shown wherever the Outlet component is located within our layout component. The Outlet component is just a placeholder component that will display the current page's content. This structure is quite handy and makes code reuse between routes incredibly simple.

The final option to share layouts with React Router is to encapsulate child Route components in a parent Route with only an element prop and no path prop.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route element={<OtherLayout />}>
    <Route path="/contact" element={<Contact />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

This code generates two routes, /contact and /about, which are both presented within the OtherLayout component. Wrapping several Route components in a parent Route component with no path attribute is useful if you want the routes to have a common layout despite having different paths.

Outlet Context

The final key thing to know about Outlet components is that they can accept a context prop, which works similarly to React context.

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

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/books/1">Book 1</Link>
          </li>
          <li>
            <Link to="/books/2">Book 2</Link>
          </li>
          <li>
            <Link to="/books/new">New Book</Link>
          </li>
        </ul>
      </nav>

      <Outlet context={{ hello: "world" }} />
    </>
  )
}
import { useParams, useOutletContext } from "react-router-dom"

export function Book() {
  const { id } = useParams()
  const context = useOutletContext()

  return (
    <h1>
      Book {id} {context.hello}
    </h1>
  )
}

In this example, we pass down a context value of { hello: "world" } to our child component, and then use the useOutletContext hook to access the value. This is a fairly typical pattern to use because you will frequently have shared data amongst all of your child components, which is the best use case for this context.

Multiple Routes

Another extremely powerful feature of React Router is the ability to use many Routes components simultaneously. This can be accomplished using either two independent Routes components or nested Routes.

Separate Routes

If you wish to present two different parts of information based on the application's URL, you'll need multiple Routes components. This is particularly frequent if, for example, you have a sidebar where you want to render specific content for specific URLs and a main page where you want to present specific material depending on the URL.

import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { BookSidebar } from "./BookSidebar"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/books">Books</Link></li>
        </ul>
      </nav>

      <aside>
        <Routes>
          <Route path="/books" element={<BookSidebar />}>
        </Routes>
      </aside>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}

In the preceding example, we have two routes. The major Routes specify all of the key components for our website, and the secondary Routes inside the aside render the sidebar for our books page when we visit /books. This means that if our URL is /books, both of our Routes components will render content because they both have a route that matches /books.

Another option with many Routes components is to hardcode the location prop.

<Routes location="/books">
  <Route path="/books" element={<BookSidebar />}>
</Routes>

By hardcoding a location prop like this, we override the normal behavior of the React Router, ensuring that no matter what the URL of our page is, this Routes component will match its Route as if it were /books.

Nested Routes

The alternative technique to use several Routes components is to stack them within one another. This is common when you have a large number of routes and wish to clean up your code by separating comparable routes into separate files.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books/*" element={<BookRoutes />} />
  <Route path="*" element={<NotFound />} />
</Routes>
import { Routes, Route } from "react-router-dom"
import { BookList } from "./pages/BookList"
import { Book } from "./pages/Book"
import { NewBook } from "./pages/NewBook"
import { BookLayout } from "./BookLayout"

export function BookRoutes() {
  return (
    <Routes>
      <Route element={<BookLayout />}>
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
        <Route path="new" element={<NewBook />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  )
}

Nesting Routes in React Router is rather simple. Simply create a new component to store your nested Routes. This component should have a Routes component, which should have all of the Route components that match the parent Route. In our situation, we are combining all of our /books routes into this BookRoute component. Then, in the parent Routes, define a Route with the same path as all of your nested Routes. In our example, this would be /books. The crucial thing to remember is that your parent Route path must conclude with a * else it will fail to match the child routes appropriately.

Essentially, the code we wrote indicates that if a route begins with /book/, it should check the BookRoutes component to see whether there is a route that matches. This is also why we include another * route in BookRoutes: to ensure that if our URL does not match any of the BookRoutes, the NotFound component is properly rendered.

useRoutes Hook

The final thing you should know about defining routes in React Router is that you may create your routes using a JavaScript object rather than JSX.

import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books">
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
      </Route>
    </Routes>
  )
}
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  const element = useRoutes([
    {
      path: "/",
      element: <Home />,
    },
    {
      path: "/books",
      children: [
        { index: true, element: <BookList /> },
        { path: ":id", element: <Book /> },
      ],
    },
  ])

  return element
}

These two components have exactly the same routes; the only variation is how they were defined. If you choose to use the useRoutes hook, all of the props you would typically send to your Route components are simply passed as key/value pairs of an object.

Handling Navigation

Now that we've defined our routes, let's talk about how to get from one to the next. This section will be divided into three pieces.

  1. Link Navigation

  2. Manual Navigation

  3. Navigation Data

First, I'd like to discuss link navigation, which is the simplest and most typical type of navigation you'll encounter. We've already seen the most basic type of link navigation with the Link component.

<Link to="/">Home</Link>
<Link to="/books">Books</Link>

However, these Link components can get quite sophisticated. For example, you can have absolute links, like as the ones above, or relative links to the current component being rendered.

<Link to="/">Home</Link>
<Link to="../">Back</Link>
<Link to="edit">Edit</Link>

Assume we are on the /books/3 path, with the links provided above. The first link will take you to the / route because it is an absolute route. Any path beginning with / is an absolute route. The second link will take you to the route /books because it is a relative link that moves up one level from /books/3 to /books. Finally, our third link will take you to the /books/3/edit page since it will append the path in the to prop to the end of the current link because it is relative.

Aside from the to prop, the Link component also requires three other props to function.

replace

The replace prop is a boolean that, if set to true, causes this link to replace the current page in the browser history. Imagine you have the following browser history.

/
/books
/books/3

If you click on a link to the /books/3/edit page with the replace property set to true, your new history will look like this.

/
/books
/books/3/edit

The page you were on was replaced with the new page. This implies that if you click the back button on the new page, you will return to the /books page rather than the /books/3 page.

reloadDocument

This prop is another boolean that is quite straightforward. If it is set to true, your Link component will behave like a standard anchor tag, performing a full page refresh on navigation rather than simply re-rendering the material within your Routes component.

state

The last prop is known as state. This prop allows you to send data along with your Link that does not appear anywhere in the URL. This is something we will go over in greater detail when we talk about navigation data, so we can disregard it for now.

The next aspect I'd want to discuss is the NavLink component. This component functions identically to the Link component, except it is designed exclusively for displaying active states on links, such as those found in navigation bars. By default, if a NavLink's to property matches the URL of the current page, the link will be assigned an active class that you can use to style it. If this isn't enough, you can pass a function with an isActive parameter to the className, style Properties, or NavLink's children.

<NavLink
  to="/"
  style={({ isActive }) => ({ color: isActive ? "red" : "black" })}
>
  Home
</NavLink>

The NavLink also contains a prop named end, which is utilized for nested routing. For example, if we're on the /books/3 page, we're rendering the Book component, which is nested within the /books route. This means that any NavLink with a to prop set to /books will be deemed active. This is because a NavLink is deemed active if the URL matches the NavLink's to prop or if the current Route is rendered within a parent component with a path that matches the NavLink's to prop.

If you do not want this default behavior, set the end prop to true, which will require the URL of the page to exactly match the to parameter of the NavLink.

Manual Navigation

Sometimes you want to manually route a user depending on criteria such as submitting a form or not having access to a particular website. For such use scenarios, you must use either the Navigate component or the useNavigation hook.

Navigate Component

The Navigate component is a very simple component that, when rendered, immediately redirects the user to the to prop of the Navigate component.

<Navigate to="/" />

The Navigate component shares all of the Link component's props, allowing you to pass it the to, replace, and state props. This component is not something I use frequently because I like to redirect a user depending on some type of interaction, such as form submission.

useNavigation Hook

In contrast, I frequently utilize the useNavigation hook. This hook is really simple, accepting no parameters and returning a single navigate function that you may use to redirect a visitor to certain URLs. This navigation function accepts two parameters. The first option is the location to which you wish to redirect the user, and the second is an object containing keys for replace and state.

const navigate = useNavigate()

function onSubmit() {
  // Submit form results
  navigate("/books", { replace: true, state: { bookName: "Fake Title" } })
}

The code above will take the user to the /books path. It will also replace the current route in history and provide some state information.

Another method to utilize the navigate function is to supply it a number. This will allow you to imitate pressing the forward or back button.

navigate(-1) // Go back one page in history
navigate(-3) // Go back three pages in history
navigate(1) // Go forward one page in history

Navigation Data

Finally, we'll speak about passing data between pages. There are three main techniques to transmit data between pages.

  1. Dynamic Parameters

  2. Search Parameters

  3. State/Location Data

Dynamic Parameters

We've already discussed how to use dynamic parameters in URLs using the useParams hook. This is the greatest method for sending information like IDs.

Search Parameters

Search parameters are all of the parameters that follow the ? in a URL (?name=Kyle&age=27). To interact with search parameters, use the useSearchParams hook, which functions similarly to the useState hook.

import { useSearchParams } from "react-router-dom"

export function SearchExample() {
  const [searchParams, setSearchParams] = useSearchParams({ n: 3 })
  const number = searchParams.get("n")

  return (
    <>
      <h1>{number}</h1>
      <input
        type="number"
        value={number}
        onChange={e => setSearchParams({ n: e.target.value })}
      />
    </>
  )
}

In this example, we have an input that, as we type, updates the search section of our URL. For example, if our input value is 32, the URL will be http://localhost:3000?n=32. The useSearchParams hook, like useState, takes an initial value, and in our case, n is set to 3. The hook then returns two values. The first item contains all of our search parameters, while the second value is a function that updates our search parameters.

The set function only accepts one input, which is the updated value of your search parameters. However, the first number, which contains the search parameters, is a little less clear. This is because the type of this value is URLSearchParams. This is why we need to use the .get syntax on line 5 above.

State/Location Data

The last type of data you can keep is state and location information. This information is all accessible using the useLocation hook. Using this hook is simple because it produces a single value and requires no parameters.

const location = useLocation()

If we had the URL http://localhost/books?n=32#id, the return value of useLocation would be as follows.

{
  pathname: "/books",
  search: "?n=32",
  hash: "#id",
  key: "2JH3G3S",
  state: null
}

This location object provides all information about our URL. It also includes a unique key that can be used for caching if you want to store information for when a user clicks the back button to return to a page. You'll see that useLocation returns a state property as well. This state data might be anything and is transmitted between pages without being saved in the URL. For example, if you click on a Link that seems like this:

<Link to="/books" state={{ name: "Kyle" }}>

The state value in the location object will be set to {name: "Kyle"}.

This is very useful if you want to communicate short messages across sites that need not be stored in the URL. A nice example of this would be a success message that appears on the page to which you are referred after creating a new book.

Routers In Depth

That covers almost everything you need to know about React Router, but there's still more to the framework. In the first portion of the basics, we discussed defining your router and highlighted the BrowserRouter and NativeRouter, but those aren't the only options. There are six routers in all, and in this section, I will go over each one in detail.

BrowserRouter

To begin, I will discuss the BrowserRouter, as that is the one we are most familiar with. This is the default router you should use if you are developing a web app, and it will be used in 99% of your apps because it supports all of the common routing use cases. Each of the other routers I'll discuss has extremely specialized use cases, so if you don't fall into one of those categories, the BrowserRouter is the one to utilize.

NativeRouter

The NativeRouter is basically the same as the BrowserRouter, except for React Native. If you're using React Native, you'll want to utilize this router.

HashRouter

This router functions similarly to the BrowserRouter, with the primary distinction being that instead of converting the URL to something like http://localhost:3000/books, it will save the URL in the hash as http://localhost:3000/#/books. As you can see, this URL ends with a #, which indicates the hash portion of the URL. Anything in the hash component of the URL is simply supplementary information that typically represents an id on the page for scrolling reasons, as a page will automatically scroll to the element with the id indicated by the hash when it loads.

In React Router, this hash is not used to hold id information for scrolling, but rather information about the current URL. React Router performs this because certain hosting providers do not allow you to modify the URL of your page. In those rare cases, you should use the HashRouter because it will only update the hash of your page and not the actual URL. If your hosting provider allows you to use any URL, you should not use this.

HistoryRouter

The HistoryRouter (AKA unstable_HistoryRouter) is a router that allows you to manually customize the history object used by React Router to store all routing information for your application. This history object serves to ensure that features like the browser's back and forward buttons work properly.

This router should probably never be used unless you have a very particular reason for wanting to modify or control React Router's default history behavior.

MemoryRouter

The MemoryRouter differs from the other routers we discussed in that it saves routing information directly in memory rather than in the browser's URL. Obviously, this is a terrible router to use for typical routing operations, but it is really handy for developing tests for your application that do not require access to the browser.

Because of the way React Router works, you must wrap your components in a router; otherwise, your routing code would give errors and break. This means that if you wish to test a single component, you must wrap it inside a router or it will give errors. If you test your code in a fashion that does not allow access to the browser (such as unit testing), the routers we've discussed so far will produce errors because they all rely on the browser for the URL. The MemoryRouter, on the other hand, maintains all of its information in memory, so it never needs to access the browser and is perfect for unit testing components. Aside from this specific use case, this router will never be used.

StaticRouter

The final router is the StaticRouter, which has a very specialized use case. This router is designed exclusively for server rendering your React apps, since it accepts a single location prop and renders your application with that location prop as the URL. This router cannot do any routing and will only produce a single static page, which is ideal for server rendering because you want to render your application's HTML on the server and then have the client set up all of your routing and so on.

<StaticRouter location="/books">
  <App />
</StaticRouter>

Conclusion

React Router is a huge framework with many fantastic features, which is why it is the most popular routing library. I really like how they handle nesting because it makes constructing intuitive and tidy routes a lot easier.