linkedin Skip to Main Content
Categories

A Comprehensive Guide to React Router

Development

Navigation routing in React is quite different from how it’s done in web development using the HTML anchor tag. This is because React is used to build single-page applications—meaning our application only loads once in our browser. This means we need a routing mechanism or logic to handle all forms of routing.

In other JavaScript frameworks like Vue, this routing logic is built by the same team that works on the core framework, meaning that we don’t need an externally written package or dependency. In React, there is no official package to handle routing; instead, we depend on the most popular and standard library, React Router.

React Router allows the browser URL to be changed and ensures the UI is in sync with the URL. It was built by Ryan Florence and Michael Jackson, who founded Remix to take React Router to the next level. They recently launched React Router version 6, whose usage has grown massively over the past year with over 8 million weekly downloads at the time of writing. Today, React Router is maintained by Remix and hundreds of other contributors.

This guide will teach you how to use React Router within your React project. We will learn how to implement React Router to ensure smooth routing within our single-page application. At the end of this guide, we will shed light on the future of React Router and how Remix plans to re-shape routing with React Router in future versions.

In this article, we will learn how to:

Install React Router

React Router is an external library that needs to be installed into our React application for us to use. We can do this by running either of the commands below in our terminal within our project’s directory:

// Using npm $ npm install [email protected]6 // Or // Using Yarn $ yarn add [email protected]6
Code language: JavaScript (javascript)

When we install the React Router package within our project, we will have access to all its components and hooks; we can now use these hooks and components within our application to enable single-page routing.

Once we have successfully installed this package, we can check our package.json file to confirm if it installed, and which version we have installed, within our dependencies object:

"dependencies": {     // rest of the dependencies installed     "react-router-dom": "^6.3.0",   },
Code language: JavaScript (javascript)

Set Up and Access Browser URL

To use React Router within our application, we need to wrap the top-level component in our React applications component hierarchy, also known as the root component (App.js), with BrowserRouter

BrowserRouter is a router implementation that uses the HTML5 History API, which comprises the pushState,replaceState, and popstate events. This History API helps track route history, keeping our UI in sync with the URL.

To wrap our root component, we can do this in the App.js or index.js file. First, we would import BrowserRouter from react-router-dom in any of the components we wish to wrap. Let’s do this in our index.js file:

// index.js // other imports import { BrowserRouter } from 'react-router-dom'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> );
Code language: JavaScript (javascript)

If we want to do this in our App.js file, we just need to ensure that everything in our App.js file is wrapped with <BrowserRouter>:

// App.js import { BrowserRouter } from "react-router-dom" const App = () => { return ( <BrowserRouter> {/* ... */} </BrowserRouter> ); }; export default App;
Code language: JavaScript (javascript)

Configure And Create Routes

We need to configure our routes to implement routing between components in our React application. Not all components will be a view (or rather, a page), so we need to declare which component is a view and assign a path to each.

We do this with two major components, which are: 

Routes: This is the parent component that wraps all our configuredRoutecomponents. This component is what React looks for when the location changes to access its children <Route> elements to find the best match and render that branch of the UI.

Route: This is the component used to declare a specific location. It holds two props which are the path and element props. The path props hold the path we would use to access the specified element. The element can be a markup or a component.

Suppose we have three components (Home, About, and Blog). Let’s configure routes for them in the App.js file:

// App.js import { Routes, Route } from 'react-router-dom'; import Home from './pages/Home'; import About from './pages/About'; import Blog from './pages/Blog'; const App = () => {   return (     <div>       <Routes>         <Route path="/" element={<Home />} />         <Route path="/about" element={<About />} />         <Route path="/blog" element={<Blog />} />       </Routes>     </div>   ); }; export default App;
Code language: JavaScript (javascript)

We first imported the two React Router components (Routes and Route) from react-router-dom in the above code. Then we import the components we would add to the element prop of each Route we configure. 

At this point, we can now navigate through our application via the URL. For example, if we navigate to /about, the About component will show. If we navigate to /blog, the Blog component will appear.

Access Configured Routes

In a real-world web application, we would not want users to navigate through routes via the URL, as that’s cumbersome to navigate. We have some components that make it easy to access these routes. Just like we have the anchor tag (<a>) in HTML5, we also have components like Link in React Router, which we can use to access our routes.

How to Use The Link Component

To use the Link component, we first need to import it from react-router-dom. This component takes in an important prop, the to prop. The to prop is used to set the component we want our UI to load:

<!-- pages/Home.js --> import { Link } from 'react-router-dom'; const Home = () => {     return (         <div className="container">             <h2>Home Page</h2>             <Link to="/about" className="btn">                 About Page             </Link>         </div>     ); }; export default Home;
Code language: HTML, XML (xml)

This works for all configured paths. We must ensure that the value passed into the to prop matches a configured Route path.

Passing State Data Through Links

We can also pass state data via our Link component, which we can access in the component we navigate into with the useLocation() router hook:

import { Link } from 'react-router-dom'; /* ... */ const userData = {   name: 'John Doe',   age: 24 } /* ... */ <Link to="/profile" state={userData}>Go to Profile</Link>
Code language: JavaScript (javascript)

We can collect this data via the useLocation() hook:

import { useLocation } from 'react-router-dom'; /*...*/ const location = useLocation(); const stateData = location.state; console.log(stateData);
Code language: JavaScript (javascript)

How to Use The NavLink Component

In some situations, such as when creating a navbar, using the Link component is not the best approach to displaying links to other views. This is because, in some instances, we want a way to know which Route is active. For example, when we are navigated to the Home page, we would want the Home Link to be styled differently from others:

Three links displayed: Home, About, and Blog. Home is underlined to indicate that it’s the current page.

We can achieve this with the NavLink component. The NavLink component is a special kind of Link component with an active class by default when a particular link is active. 

// /components/Navbar.js import { NavLink } from 'react-router-dom'; const Navbar = () => {     return (         <nav className="navbar">             <NavLink to="/">Home</NavLink>             <NavLink to="/about">About</NavLink>             <NavLink to="/blog">Blog</NavLink>         </nav>     ); }; export default Navbar;
Code language: JavaScript (javascript)

We can use this active class to add styling in whichever way we prefer. For example, we can create a style for active in CSS this way:

/* index.css */ .navbar a {     text-decoration: none; } .navbar a.active {     color: #333;     text-decoration: underline; }
Code language: CSS (css)

Handle No Routes without Matches

At this point, we have learned how to configure a route and access the configured Route. The next big question that will pop into our minds is, “What happens when we navigate a route that is not configured?”

The answer is nothing. Nothing happens. The path will match no route, meaning nothing will show on our screen. When we check our console, a warning error appears with the message No routes matched location "/fff" because we tried routing to /fff, which is not configured.

We can fix this by setting up a no-match route, which will hold a component. Let’s set one up as a “not found” page or make a game out of missing pages, or more. This component will be displayed on our UI when a route does not match the configured Route. We can configure this no-match Route in the &lt;Routes&gt; configuration on our App.js file:

<!-- App.js --> // other imports import Home from './pages/Home'; import Error from './pages/Error'; const App = () => {   return (     <div>       <Routes>         // other configured routes         <Route path="/" element={<Home />} />         <Route path="*" element={<Error />} />       </Routes>     </div>   ); }; export default App;
Code language: JavaScript (javascript)

A route with an asterisk path * is a no-match route. It only matches when no other routes do. In the above example, we linked it to the Error page component, which is the 404 no page found component:

import { Link } from 'react-router-dom'; const Error = () => {     return (         <div className="container">             <h2>404</h2>             <p>Page not found!</p>             <Link to="/" className="btn">                 Go Home             </Link>         </div>     ); }; export default Error;
Code language: JavaScript (javascript)

Implement Nested Routes

Nested routes are an aspect of React Router we need to understand; this feature enables us to handle routing with a different approach. It allows you to exchange specific view fragments based on the current Route.

For example, if we have a user’s page on which we only want a particular section (outlet) to change while the other section remains fixed (shared layout), we can use nested routing.

Imagine there are four routes to an app: Profile, Posts, Friends and settings. You can have an “outlet” to display the individual page contents depending on the URL while having a shared “layout” component to display contents like an app’s navigation sidebar.

This is what the routes configuration will look like in our App.js file:

// App.js // Other imports const App = () => {   return (     <div>       <Routes>         // Other configured routes         <Route path="/user" element={<DashboardLayout />} >           <Route index element={<Profile />} />           <Route path="/posts" element={<Posts />} />           <Route path="/friends" element={<Friends />} />           <Route path="/settings" element={<Settings />} />         </Route>       </Routes>     </div>   ); }; export default App;
Code language: JavaScript (javascript)

Let’s now explain how it works. By using a nested Route, we will have a layout that would serve as a structure for how our page will look like. This layout will hold all static data and will create a space where we want the children’s routes (nested routes) to appear using the Outlet component:

import { Outlet } from 'react-router-dom'; const DashboardLayout = () => {     return (         <div>             <div className="sidebar">                 {/* Side bar Links */}             </div>             <div className="main-section">                 <div className="title">                     {/* Dashboard Title */}                 </div>                 <Outlet />             </div>         </div>     ); }; export default DashboardLayout;
Code language: JavaScript (javascript)

Having passed the Outlet component, any nested component of this layout route will be displayed in the outlet section.

How to Create an Index Route

Now you may begin to ask yourself what will show by default. For example, when we navigate to user/posts, the posts component will show within the outlet portion of our layout. When we navigate to user/friends, the friends component will show in the outlet of our layout. But what will appear in the outlet when we navigate to the /user path? This is where the index route is applicable.

The index route is used in nested routing. Any component with the index route will appear in the outlet section of our layout. This route is identified with the index props, which receives no value:

<Route path="/user" element={<DashboardLayout />} >   <Route index element={<Profile />} /> </Route>
Code language: JavaScript (javascript)

Configure Parameter-Enhanced Route

There are two approaches to router pathing: standard routes and parameter-enhanced routes. So far, we have considered various ways to implement standard routing, but now, let’s understand how to add parameters to our routing. 

Using parameter routes allows us to render content on our UI conditionally. We use data passed via our routes as conditions to decide which data shows up on our UI, without changing layouts or pages completely.

For example, if we have a list of blog posts we want to display each post’s full content on a specific page. Using the non-parmeter routing method, we would have to create individual Routes and identical components for each post, which can become challenging, or even impossible, to maintain.

Parameter-based routing allows us to pass parameters through the path, as the name implies. We can use that to query which content appears on our page. Let’s now see how to configure a parameter route in our App.js file:

import { Routes, Route } from 'react-router-dom'; import Blog from './pages/Blog'; import SinglePost from './pages/SinglePost'; const App = () => {   return (     <div>       <Routes>           <Route path="/blog" element={<Blog />} />           <Route path="/blog/:id" element={<SinglePost />} />       </Routes>     </div>   ); }; export default App;
Code language: JavaScript (javascript)

In the above, we created a route with a parameter value of id. This value can be any string. For us to access this page and load up this page on our UI, the user will need to navigate to a URL like /blog/1. We can replace 1 with any string, and the page will load up.

Our path can be anything based on how we want it or the parameter we want to pass to the SinglePost component. We can have as many parameters as /blog/:category/:id.

Note: Adding a colon before the path name shows that the value is a parameter.

We fetched an array of blogposts on our blog page from a local data file. And then added a link that passes in the ID to the path dynamically:

import { Link } from 'react-router-dom'; import posts from './data'; const Blog = () => {     return (         <div className="container">             <h2>Blog Page</h2>             <div className="posts">                 {posts.map((post) => (                     <div className="post" key={post.id}>                         <h3>{post.title}</h3>                         <p>{post.body}</p>                         <Link to={`/blog/${post.id}`} className="btn">                             Read More                         </Link>                     </div>                 ))}             </div>         </div>     ); }; export default Blog;
Code language: JavaScript (javascript)

Let’s now see how we can fetch this URL parameter and use it to query for data within our component.

Get URL Parameters With useParams Hook

At this point, when a user navigates a route with any ID, the page shows up, but we want to get this parameter (ID) and use it to fetch the particular blog post the user wants to see. We do this with the useParams hook.

The useParams hook gives us access to an object containing all the parameters we have passed using the path name as the object key. For example, if our path is user/:id, our object will be {id: 4}.

To use the useParams() hook, we first import it from react-router-dom and then get our data. In our case, we will destructure to get the id right away:

import { useParams } from 'react-router-dom'; import posts from '../data'; const SinglePost = () => {     const { id } = useParams();         return (         // ...     ); }; export default SinglePost;
Code language: JavaScript (javascript)

We can now query for the specific post we want to showcase with the id param. Depending on how we are fetching our data, we might have to perform an HTTP request if it’s an API, but since we are using a local data file, we just need to find which posts id will match the id parameter:

import { useParams } from 'react-router-dom'; import posts from '../data'; const SinglePost = () => {     const { id } = useParams();     const post = posts.find((post) => post.id == id);     const { title, body } = post;     return (         <div className="container">             <h2>{title}</h2>             <p>{body}</p>         </div>     ); }; export default SinglePost;
Code language: JavaScript (javascript)

Implement Programmatic Navigation

Programmatic navigation can be seen as the process of navigating or redirecting a user based on specific actions. For example, if a user clicks a button, signs in or signs out, or submits a form.

In React Router, this done with the useNavigate() hook.

Implement Programmatic Navigation With useNavigate Hook

The useNavigate() hook allows us to perform all forms of programmatic routing, such as navigation redirects and lots more.

To use this hook, we would first import it from react-router-dom and then initialize the navigate method, so we can use it on any button or perform all forms of programmatic navigation.

import { useNavigate } from 'react-router-dom'; const navigate = useNavigate();
Code language: JavaScript (javascript)

Let’s start by seeing how to navigate backward when a button is clicked:

<button onClick={() => navigate(-1)}>Back</button>
Code language: HTML, XML (xml)

We can also access a route programmatically:

import { useNavigate } from 'react-router-dom'; const Blog = () => {     const navigate = useNavigate();     const naviagteHome = () => {         navigate('/');     };     return (         <div>             <h1>Blog page</h1>             {/*  */}             <button onClick={naviagteHome}>Go Back Home</button>         </div>     ); }; export default Blog;
Code language: JavaScript (javascript)

The navigate hook also makes it possible to pass state values via these routes, which can be retrieved with the useLocation() hook.

navigate("/blog", {state: {authorDetails}});
Code language: JavaScript (javascript)

And just like we did earlier, we can access these state data via the useLocation() hook:

import { useLocation } from "react-router-dom"; /*...*/ const location = useLocation(); const authorData = location.state; console.log(authorData);
Code language: JavaScript (javascript)

Code Splitting With React Router

When React applications and other SPAs load the initial landing page for a user’s request it simultaneously loads the code for every other page as well, leading to a massive bundle size and a poor user experience. Code splitting allows us to break down our application so that only needed components are loaded.

When we implement code splitting with React Router, our application will only load the components we have routed. We can do this with React.lazy() and React.Suspense.

The function React.lazy() enables us to render dynamic imports similarly to normal components. In contrast, React.Suspense enables us to specify the fallback prop, which takes placeholder content that would be used as a loading indicator.

For example, let’s implement lazy loading in our Routes configuration by using just a few routes in the App.js file:

import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Blog = lazy(() => import('./pages/Blog')); const App = () => {   return (     <div>       <Suspense fallback={<div>Loading...</div>}>         <Routes>           <Route path="/" element={<Home />} />           <Route path="/about" element={<About />} />           <Route path="/blog" element={<Blog />} />         </Routes>       </Suspense>     </div>   ); }; export default App;
Code language: JavaScript (javascript)

Remix: The Future of React Router

On March 23rd 2022, Ryan Florence published an article on the Remix blog, “Remixing React Router”. This post contained a lot of downsides the current React Router has and how they have started upgrading the current package to include some remix actions to solve these problems.

Currently, we fetch data within our components, but most of the time, we want the data to load up immediately after our application renders. This means that we perform a render+fetch request. This definitely will slow down our page load time and affects user experience (UX). When performing these render+fetch requests in nested components, your app is likely to become slow.

Here is an example from the blog post, if we have nested routes in which each component fetches data from a server:

<Routes>   <Route element={<Root />}>     <Route path="projects" element={<ProjectsList />}>       <Route path=":projectId" element={<ProjectPage />} />     </Route>   </Route> </Routes>
Code language: JavaScript (javascript)

In a situation whereby the Root component requests to get the user details, the ProjectList component requests to get the project lists of the user, and then the ProjectPage component requests to get a single project with its details based on the passed Id. Performing all these requests one-by-one would take longer than having to perform all these requests at once.

Instead of 3 assets loading one-by-one, 1 second at a time, within 3 seconds, they can all load simultanously in 1 second. Image by Ryan Florence

In the future version of React Router, which is currently being released in development mode, many changes and improvements are coming, all intending to improve user experience. When it comes to data fetching, data will be fetched when declaring routes and passed into the components to consume immediately using the useLoaderData() hook.

<Route     path=":projectId"     element={<Projects />}     loader={async ({ signal, params }) =>       fetch(`/api/projects/${params.projectId}`, { signal })     }   /> </Route>
Code language: JavaScript (javascript)

We will consume the data in our application this way:

let data = useLoaderData();
Code language: JavaScript (javascript)

You can read more here and watch the recent talk by Ryan Florence at the Real World React conference.

Wrapping Up

In this guide, we have learned much about React Router Version Six and how it works to help us implement single-page routing in our React applications. We also learned how the next version of React Router would help make the user experience better and faster.

Thanks for reading, and have fun coding!

I’m Joel Olawanle, a frontend developer and technical writer interested in making the web accessible to everyone by always looking for ways to give back to the tech community. Follow me on Twitter.