React Router allows us to route to different “pages” even on a single-page app. Learn how to implement it and style its links.
Most frontend apps have more than one page of content. In a single-page app, we add multiple pages of data by routing URLs to show different content.
React does not have a routing library built in. Therefore, we have to add one ourselves.
React Router is a popular routing library for React apps. In this article, we will look at how to use React Router v6 to add routing capabilities to our React apps.
To use it, we install the react-router-dom
package.
To do this, we run:
npm i react-router-dom
Then we write:
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
App.js
import * as React from "react";
import { createBrowserRouter, RouterProvider, Link } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<Link to="about">About Us</Link>
<div>Hello World</div>
</div>
),
},
{
path: "about",
element: <div>About</div>,
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
We call createBrowserRouter
to create a router object. We call it with an array of routes.
The path
is mapped to the element
being rendered. Therefore, when we go to the path
, we see the content in element
rendered.
The Link
component is used to render a link. And the to
prop is set to the route URL we want to go in the app.
We use RouterProvider
with the router
prop set to router
to render the routes when we go to the URLs listed in the path
properties.
We can also add nested routes easily. To do this, we can nest the routes in the children
array.
For example, we write:
App.js
import * as React from "react";
import {
createBrowserRouter,
RouterProvider,
redirect,
Outlet,
useLoaderData,
} from "react-router-dom";
const Dashboard = () => {
const data = useLoaderData();
return <div>dashboard {JSON.stringify(data)}</div>;
};
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
hello <Outlet />
</div>
),
children: [
{
path: "contact",
element: <div>contact</div>,
},
{
path: "dashboard",
element: <Dashboard />,
loader: ({ request }) =>
fetch("https://yesno.wtf/api", {
signal: request.signal,
}),
},
{
element: (
<div>
auth <Outlet />
</div>
),
children: [
{
path: "login",
element: <div>login</div>,
},
{
path: "logout",
action: <div>logout</div>,
loader: () => redirect("/login"),
},
],
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
to add nested routes by putting them in the children
array. We use the Outlet
component to render nested route contents.
loader
is set to a function that returns a promise.
request.signal
is an abort signal object that we can use to cancel the request if we want to.
To get the value returned by the promise, we use the useLoaderData
hook. In Dashboard
, we just call it to get the returned data.
We add another nested route in the last children
array with the login
and logout
routes. In the logout
route, we redirect to the login
route with the redirect
function.
We can add route placeholder segments by starting them with semicolons.
For instance, we write:
import * as React from "react";
import {
createBrowserRouter,
RouterProvider,
Outlet,
useLoaderData,
useParams,
} from "react-router-dom";
const Dashboard = () => {
const data = useLoaderData();
const params = useParams();
return (
<div>
dashboard {JSON.stringify(data)} {JSON.stringify(params)}
</div>
);
};
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
hello <Outlet />
</div>
),
children: [
{
path: "dashboard/:parentId/child/:childId",
element: <Dashboard />,
loader: ({ params }) => {
return params;
},
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
to add the :parentId
and :childId
route placeholders.
Then we call useParams
in the Dashboard
component to get an object with the parentId
and childId
properties which have the route parameters.
The loader
function also has a params
property in the parameter object. And we can do things with it or return it. If it’s returned, we can get the values in useLoaderData
as we do. And the result is the same value as what is returned with useParams
.
We can get and set query strings with the useSearchParams
hook.
To do this, we write:
import * as React from "react";
import {
createBrowserRouter,
RouterProvider,
Outlet,
useSearchParams,
} from "react-router-dom";
const Dashboard = () => {
const [searchParams, setSearchParams] = useSearchParams();
console.log(searchParams.toString());
return (
<button onClick={() => setSearchParams({ foo: 1, bar: 2 })}>go</button>
);
};
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
hello <Outlet />
</div>
),
children: [
{
path: "dashboard",
element: <Dashboard />,
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
to add the useSearchParams
hook to the Dashboard
component.
In it, we call useSearchParams
to return an array with the getter and setter function.
We get the query string value with searchParams
and we call setSearchParams
to set the query string.
searchParams
is a regular URLSearchParams
object built into the browser so we can call toString
to get the query string.
We call setSearchParams
with an object with the query parameter keys and values to redirect to the URL with the query string.
React Router v6 comes with the NavLink
component that lets us style links easily depending on whether we are on the page that it links to or not.
For instance, we can use it by writing:
import * as React from "react";
import {
createBrowserRouter,
RouterProvider,
Outlet,
NavLink,
} from "react-router-dom";
const Root = () => {
return (
<div>
<NavLink
style={({ isActive }) => {
return {
color: isActive ? "red" : "inherit",
};
}}
className={({ isActive, isPending }) => {
return isActive ? "active" : isPending ? "pending" : "";
}}
to="/"
>
home
</NavLink>
<NavLink
style={({ isActive }) => {
return {
color: isActive ? "red" : "inherit",
};
}}
className={({ isActive, isPending }) => {
return isActive ? "active" : isPending ? "pending" : "";
}}
to="dashboard"
>
dashboard
</NavLink>
<NavLink
style={({ isActive }) => {
return {
color: isActive ? "red" : "inherit",
};
}}
className={({ isActive, isPending }) => {
return isActive ? "active" : isPending ? "pending" : "";
}}
to="contact"
>
contact
</NavLink>
<Outlet />
</div>
);
};
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "/",
element: <div>home</div>,
},
{
path: "dashboard",
element: <div>dashboard</div>,
},
{
path: "contact",
element: <div>contact</div>,
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
to add the Root
component.
In it, we add NavLink
components to add links that we can style depending on whether we are on the page the link links to or not.
We set the style
prop to a function that takes an object with the isActive
property.
If isActive
is true
, then we are on the page that the link links to. And we set the link color to red in that case. Otherwise, we set it to the default inherited color.
The isPending
property is also available in the object parameter so we can check whether navigation to the route is pending.
Likewise, we can set the className
property to a function that takes the same object. And we can return class name strings for the link depending on the state of navigation as we do with the function we set as the value of the style
prop.
Now we can see that the link turns red when we are on the page that the link links to.
React Router is a simple-to-use routing library that we can use to create React apps with multiple pages. It has all the functionality like query string and URL parameter handling built in. We can also use it to do things when a route is loading.
And it also comes with an easy way to render links to different routes and style them depending on whether we’re already on the linked page.
John Au-Yeung is a frontend developer with 6+ years of experience. He is an avid blogger (visit his site at https://thewebdev.info/) and the author of Vue.js 3 By Example.