Web Dev Simplified Blog

Ultimate React Router v6 Guide

July 25, 2022

React Router is the most popular routing library in React, but it can be a bit complicated to wrap your head around some of the more complex features. That is why in this article I will be breaking down everything you need to know about React Router so you can use even the most advanced features with ease. This article will be broken down into 4 sections.

  1. React Router Basics
  2. Advanced Route Definitions
  3. Handling Navigation
  4. Routers In Depth

If you prefer to learn visually, check out the video version of this article.

React Router Basics

Before we start diving into the advanced features of React Router, I first want to talk about the basics of React Router. In order to use React Router on the web you need to run npm i react-router-dom to install React Router. This library specifically installs the DOM version of React Router. If you are using React Native you will need to install react-router-native instead. Other than this one small difference the libraries work almost exactly the same.

In this tutorial I will be focusing on react-router-dom, but as I said both libraries are nearly identical.

Once you have this library there are three things you need to do in order to use React Router.

  1. Setup your router
  2. Define your routes
  3. Handle navigation

Configuring The Router

The easiest step by far is setting up your router. All you need to do is import the specific router you need (BrowserRouter for the web and NativeRouter for mobile) and wrap your entire application in that router.

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

Generally you will import your router in the index.js page of your application and it will wrap your App component. The router works just like a context in React and provides all the necessary information to your application so you can do routing and use all the custom hooks from React Router.

Defining Routes

The next step in React Router is to define your routes. This is generally done at the top level of your application, such as in the App component, but can be done anywhere you want.

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 defining a single Route component for each route in your application and then putting all those Route components in a single Routes component. Whenever your URL changes React Router will look at the routes defined in your Routes component and it will render the content in the element prop of the Route that has a path that matches the URL. In the above example if our URL was /books then the BookList component would be rendered.

The nice thing about React Router is that when you navigate between pages it will only refresh the content inside your Routes component. All the rest of the content on your page will stay the same which helps with performance and user experience.

Handling Navigation

The final step to React Router is handling navigation. Normally in an application you would navigate with anchor tags, but React Router uses its own custom Link component to handle navigation. This Link component is just a wrapper around an anchor tag that helps ensure all the routing and conditional re-rendering is handled properly so you can use it just like your would a normal 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 added two links to the home and books page. You will also notice that we used the to prop to set the URL instead of the href prop you are used to using with an anchor tag. This is the only difference between the Link component and an anchor tag and is something that you need to remember as it is an easy mistake to accidentally use an href prop instead of the to prop.

Another thing to note about our new code is that the nav we are rending at the top of our page is outside of our Routes component which means when we change pages this nav section 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 really gets interesting. There is a lot of cool stuff you can do with routing to make more complex routes, easier to read, and overall much more functional. This can be done through five main techniques.

  1. Dynamic Routing
  2. Routing Priority
  3. Nested Routes
  4. Multiple Routes
  5. useRoutes Hook

Dynamic Routing

The simplest and most common advanced feature in React Router is handling dynamic routes. In our example, let’s assume that we want to render out a component for individual books in our application. We could hardcode each of those routes, but if we have hundreds of books or the ability for users to create books then it is impossible to hardcode all these routes. Instead we need a dynamic route.

<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 that has a dynamic parameter of :id. Defining dynamic routes in React Router is as simple as putting a colon in front of whatever you want the dynamic part of your route to be. In our case our dynamic route will match any URL that starts with /book and ends with some value. For example, /books/1, /books/bookName, and /books/literally-anything will all match our dynamic route.

Pretty much always when you have a dynamic route like this you want to access the dynamic value in your custom 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 takes no parameters and will return an object with keys that match the dynamic parameters in your route. In our case our dynamic parameter is :id so the useParams hook will return an object that has a key of id and the value of that key will be the actual id in our URL. For example, if our URL was /books/3 our page would render Book 3.

Routing Priority

When we were just dealing with hard coded routes it was pretty easy to know which route would be rendered, but when dealing with dynamic routes it can be a bit more complicated. Take these routes for example.

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

If we have the URL /books/new which route would this match? Technically, we have two routes that match. Both /books/:id and /books/new will match since the dynamic route will just assume that new is the :id portion of the URL so React Router needs another way to determine which route to render.

In older versions of React Router whichever route was defined first would be the one that is rendered so in our case the /books/:id route would be rendered which is obviously not what we want. Luckily, version 6 of React Router changed this so now React Router will use an algorithm to determine which route is most likely the one you want. In our case we obviously want to render the /books/new route so React Router will select that route for us. The actual way this algorithm works is very similar to CSS specificity since it will try to determine which route that matches our URL is the most specific (has the least amount of dynamic elements) and it will select that route.

While we are on the topic of routing priority I also want to talk about how to create a route that matches 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 * will match anything at all which makes it perfect for things like a 404 page. A route that contains a * will also be less specific than anything else so you will never accidentally match a * route when another route would have also matched.

Nested Routes

Finally, we have come to my favorite part of React Router which is how they handle route nesting. In the above example we have three routes that start with /books so we can nest those routes 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 pretty simple to do. All you need to do is make a parent Route that has the path prop set to the shared path for all your child Route components. Then inside the parent Route you can put all the child Route components. The only difference is that the path prop of the child Route components no longer includes the shared /books route. Also, the route for /books is replaced with a Route component that has no path prop, but instead has an index prop. All this is saying is that the path of the index Route is the same as the parent Route.

Now if this is all you could do with nested routes it would be only marginally useful, but the true power of nested routes comes in how it handles shared layouts.

Shared Layouts

Let’s imagine that we want to render a nav section with links to each book as well the new book form from any of our book pages. To do this normally we would need to make a shared component to store this navigation and then import that into every single book related component. This is a bit of a pain, though, so React Router created its own solution to solve this problem. If you pass an element prop to a parent route it will render that component for every single child Route which means you can put a shared nav or other shared components on every child page with ease.

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

The way our new code will work is whenever we match a route inside the /book parent Route it will render the BooksLayout component which contains our shared navigation. Then whichever child Route is matched will be rendered wherever the Outlet component is placed inside our layout component. The Outlet component is essentially a placeholder component that will render whatever our current page’s content is. This structure is incredibly useful and makes sharing code between routes incredibly easy.

Now the final way you can share layouts with React Router is by wrapping child Route components in a parent Route that only defines 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 bit of code will create two routes, /contact and /about, which both are rendered inside the OtherLayout component. This technique of wrapping multiple Route components in a parent Route component with no path prop is useful if you want those routes to share a single layout even if they don’t have a similar path.

Outlet Context

The final important thing to know about Outlet components is they can take in a context prop which will work just like 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>
  )
}

As you can see from this example, we are passing down a context value of { hello: "world" } and then in our child component we are using the useOutletContext hook to access the value for our context. This is a pretty common pattern to use since often you will have shared data between all your child components which is the ideal use case for this context.

Multiple Routes

Another incredibly powerful thing you can do with React Router is use multiple Routes components at the same time. This can be done as either two separate Routes components or as nested Routes.

Separate Routes

If you want to render two different sections of content that both depend on the URL of the application then you need multiple Routes components. This is very common if for example you have a sidebar you want to render certain content in for certain URLs and also a main page that should show specific content based 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 above example we have two Routes. The main Routes defines all the main components for our page and then we have a secondary Routes inside the aside that will render the sidebar for our books page when we are at /books. This means if our URL is /books both of our Routes components will render out content since they both have a unique match for /books in their Routes.

Another thing that you can do with multiple Routes components is hardcode the location prop.

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

By hardcoding a location prop like this we are overriding the default behavior or React Router so no matter what the URL of our page is this Routes component will match its Route as if the URL was /books.

Nested Routes

The other way to use multiple Routes components is to nest them inside one another. This is pretty common if you have lots of routes and want to clean up your code by moving similar routes into their own 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 pretty simple. All you need to do is create a new component to store your nested Routes this component should have a Routes component and inside that Routes component should be all the Route components that you are matching with the parent Route. In our case we are moving all our /books routes into this BookRoute component. Then in the parent Routes you need to define a Route that has a path equal to the path all your nested Routes share. In our case that would be /books. The important thing, though, is you need to end your parent Route path with a * otherwise it will not properly match the child routes.

Essentially, the code we have written says that whenever a route starts with /book/ it should search inside the BookRoutes component to see if their is a Route that matches. This is also why we have another * route in BookRoutes so that we can ensure if our URL does not match any of the BookRoutes it will properly render the NotFound component.

useRoutes Hook

The final thing you need to know about defining routes in React Router is that you can use a JavaScript object to define your routes instead of JSX if you prefer.

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 both have the exact same routes the only difference is how they were defined. If you do decide you want to use the useRoutes hook all the props that you would normally pass to your Route components are instead just passed as key/value pairs of an object.

Handling Navigation

Now that we know how to define our routes we need to talk about how to navigate between those routes. This section will be broken down into three sections.

  1. Link Navigation
  2. Manual Navigation
  3. Navigation Data

First I want to talk about link navigation since it is the simplest and most common form of navigation you will encounter. We have already seen the most basic form of link navigation using the Link component

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

These Link components can get a bit more complex, though. For example you can have absolute links like the above links or you can have links that are relative to the current component being rendered.

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

For example imagine we are in the /books/3 route with the above links. The first link will lead to the / route since it is an absolute route. Any route that starts with a / is an absolute route. The second link will lead to the route /books since it is a relative link that goes up one level from /books/3 to /books. Finally, our third link will go to the /books/3/edit page since it will add the path in the to prop to the end of the current link since it is a relative link.

Besides the to prop, there are also 3 other props that are important to the Link component.

replace

The replace prop is a boolean that when set to true will cause 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 that goes to the /books/3/edit page but it has the replace property set to true your new history will look like this.

/
/books
/books/3/edit

The page your were currently on was replaced with the new page. This means that if you click the back button on the new page it will bring you back to the /books page instead of the /books/3 page.

reloadDocument

This prop is another boolean and is very simple. If it is set to true your Link component will act like a normal anchor tag and do a full page refresh on navigation instead of just re-rendering the content inside your Routes component.

state

The final prop is called state. This prop lets you pass data along with your Link that does not show up anywhere in the URL. This is something we will cover in more depth when we talk about navigation data so we can ignore it for now.

The next element I want to talk about is the NavLink component. This component works exactly the same as the Link component, but it is specifically for showing active states on links, for example in nav bars. By default if the to property of a NavLink is the same as the URL of the current page the link will have an active class added to it which you can use for styling. If this is not enough you can instead pass a function with an isActive parameter to the className, or style props, or as the children of the NavLink.

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

The NavLink also has one prop called end which is used to help with nested routing. For example, if we are on the /books/3 page that means we are rendering the Book component which is nested inside our /books route. This means that if we have a NavLink with a to prop set to /books it will be considered active. This is because a NavLink is considered active if the URL matches the to prop of the NavLink or if the current Route being rendered is inside a parent component that has a path that matches the to prop of the NavLink. If you do not want this default behavior you can set the end prop to true which will make it so the URL of the page must exactly match the to prop of the NavLink.

Manual Navigation

Now sometimes you want to manually navigate a user based on things like submitting a form or not having access to a specific page. For those use cases you will need to either use the Navigate component or the useNavigation hook.

The Navigate component is a really simple component that when rendered will automatically redirect the user to the to prop of the Navigate component.

<Navigate to="/" />

The Navigate component shares all the props of the Link component so you can pass it the to, replace, and state props. This component is not really something I use much as more often than not I want to redirect a user based on some form of interaction like a form submission.

useNavigation Hook

The useNavigation hook on the other hand is a hook I use all the time. This hook is a really simple hook that takes no parameters and returns a single navigate function which you can use to redirect a user to specific pages. This navigate function takes two parameters. The first parameter is the to location you want to redirect the user to and the second parameter is an object that can have keys for replace, and state.

const navigate = useNavigate()

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

The above code will redirect the user to the /books route. It will also replace the current route in history and pass along some state information as well.

Another way you can use the navigate function is to pass it a number. This will allow you to simulate hitting the forward/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

Finally it is time to talk about passing data between pages. There are 3 main ways you can pass data between pages.

  1. Dynamic Parameters
  2. Search Parameters
  3. State/Location Data

Dynamic Parameters

We have already talked about how to use dynamic parameters in URLs by using the useParams hook. This is the best way to handle passing information like ids.

Search Parameters

Search parameters are all of the parameters that come after the ? in a URL (?name=Kyle&age=27). In order to work with search parameters you need to use the useSearchParams hook which works very 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 in will update the search portion of our URL. For example if our input has the value of 32 our URL will look like http://localhost:3000?n=32. The useSearchParams hook takes an initial value just like useState and in our case our initial value has n set to 3. This hook then returns two values. The first value is all our our search parameters and the second value is a function for updating our search parameters. The set function just takes a single argument that is the new value of your search parameters. The first value that contains the search parameters is a bit more confusing, though. This is because this value is of the type URLSearchParams. That is why we need to use the .get syntax on line 5 above.

State/Location Data

The final type of data you can store is state and location data. This information is all accessible via the useLocation hook. Using this hook is very simple as it returns one value and takes no parameters.

const location = useLocation()

If we have the following URL http://localhost/books?n=32#id then the return value of useLocation would look like this.

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

This location object contains all the information related to our URL. It also contains a unique key that you can use to do caching if you want to cache information for when a user clicks the back button to come back to a page. You also will notice that we have a state property being returned from useLocation as well. This state data can be anything and is passed between pages without being stored in the URL. For example if you click on a Link that looks like this:

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

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

This can be really useful if for example you wan to send across simple messages between pages that shouldn’t be stored in the URL. A good example of this would be something like a success message that gets sent to the page you are redirected to after creating a new book.

Routers In Depth

Now that covers 95% of what you need to know about React Router, but the library has a bit more depth still. In the first section of the basics we talked about defining your router and we mentioned the BrowserRouter and NativeRouter, but those are not the only routers. There are actually 6 routers in total and in this section I will be explaining each of them in depth.

BrowserRouter

To start I will cover the BrowserRouter since it is the one we are already familiar with. This is the default router you should use if you are working on a web app and is the router you will use in 99% of all your applications since it covers all the normally routing use cases you have. Each of the other routers I will talk about have very specific use cases where you would want to use them so if you don’t fit those use cases then the BrowserRouter is what you should use.

NativeRouter

The NativeRouter is essentially the equivalent of the BrowserRouter, but for React Native. If you are using React Native then this is the router you will want to use.

HashRouter

This router works very similarly to the BrowserRouter, but the main difference is that instead of changing the URL to something like http://localhost:3000/books it will store the URL in the hash like so http://localhost:3000/#/books. As you can see this URL has a # after the URL which represents the hash portion of the URL. Anything in the hash portion of the URL is just additional information that usually denotes an id on the page for scrolling purposes since a page will automatically scroll to the element with the id represented by the hash when the page loads.

In React Router this hash is not actually used to store id information for scrolling, but instead it stores information related to the current URL. The reason React Router does this is because some hosting providers do not allow you to actually change the URL of your page. In those very rare circumstances you will want to use the HashRouter since the HashRouter will not change the actual URL of your page and will only change the hash of your page. If you are able to use any URL with your hosting provider then this is not something you should use.

HistoryRouter

The HistoryRouter (currently called unstable_HistoryRouter) is a router that allows you to manually control the history object that React Router uses to store all the information related to the history of your application’s routing. This history object helps make sure things like the back and forward button in the browser work properly.

This is a router you should probably never use unless you have a very specific reason that you want to overwrite or control the default history behavior of React Router.

MemoryRouter

The MemoryRouter is a bit different than the rest of the routers we talked about since, instead of storing information about the current route in the URL of the browser, this router stores information on routing directly in memory. Obviously, this is a very bad router to use for normal routing operations, but this router is incredibly useful when you are writing tests for your application that do not have access to the browser.

Because of how React Router works, you need to have your components wrapped in a router otherwise all your routing code will throw errors and break. This means even if you want to test a single component, you will need to wrap that component inside of a router or it will throw errors. If you are testing your code in a way that it does not have access to the browser (such as unit testing) then the routers we have talked about so far will throw errors since they all depend on the browser for the URL. The MemoryRouter on the other hand stores all its information in memory which means it never accesses the browser and is ideal when trying to unit test components. Other than this specific use case, though, this router is never to be used.

StaticRouter

The final router is the StaticRouter and this router again has a very specific use case. This router is specifically meant for server rendering your React applications since it takes in a single location prop and renders out your application using that location prop as the URL. This router cannot actually do any routing and will just render a single static page, but that is perfect for server rendering since you want to just render the HTML of your application on the server and then the client can set up all your routing and so on.

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

Conclusion

React Router is a massive library with tons of amazing features which is the reason it is the go-to routing library for most people. I really love how they handle things like nesting since it makes creating intuitive and clean routes so much easier.