Mastering React Router: A Comprehensive Guide
Step-by-Step Tutorial with Code Examples for Seamless Navigation in React
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.
Router Configuration
Routes Definition
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.
Dynamic Routing
Routing Priority
Nested Routes
Multiple Routes
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.
Link Navigation
Manual Navigation
Navigation Data
Link Navigation
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.
NavLink
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.
Dynamic Parameters
Search Parameters
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.