React Router

React Router is a popular library for managing routing in React applications. It enables you to create SPAs by defining routes and rendering components based on the current URL.

Poetically, React Router follows a similar ethos, except instead of UI primitives, they give you routing primitives. To align with React, naturally, these “routing primitives” are really just a collection of React components and Hooks.

Installation

Installation

npm install react-router-dom

Understanding Routing Concepts

Before we continue, let’s have a quick understanding of some key concepts in React Router:

  • History Stack: React router intelligently uses the HTML5 History API to keep track of the user’s navigation history. When a user visits a new URL, React Router pushes that URL to the history stack. As the user navigates to other URLs, they are also pushed to the stack. This is how React Router can navigate back and forth between routes without refreshing the page.
  • Location: This refers to the URL that you’re currently on when navigating through a website. It is the topmost URL in the history stack and how React Router dynamically changes the content displayed to match the correct URL.

Basics

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

  • ✅ Setup your router
  • ✅ Define your routes
  • ✅ Handle navigation

Browser router

Naturally, in order to do its thing, React Router needs to be both aware and in control of your app’s location. The way it does this is with its BrowserRouter component.

Unser The hood

Under the hood, BrowserRouter uses both the history library as well as React Context. The history library helps React Router keep track of the browsing history of the application using the browser’s built-in history stack, and React Context helps make history available wherever React Router needs it.

There's not much to BrowserRouter, you just need to make sure that if you're using React Router on the web, you wrap your app inside of the BrowserRouter component.

Configuring The Router

This is the recommended router for all React Router web projects. It uses the DOM History API to update the URL and manage the history stack. It also enables the v6.4 data APIs like loadersactionsfetchers and more.

 
 
import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
 
import Root, { rootLoader } from "./routes/root";
import Team, { teamLoader } from "./routes/team";
 
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);
 
ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />

type declaration

function createBrowserRouter(
  routes: RouteObject[],
  opts?: {
    basename?: string
    future?: FutureConfig
    hydrationData?: HydrationState
    window?: Window
  },
): RemixRouter

Route

An array of Route objects with nested routes on the children property.

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "events/:id",
        element: <Event />,
        loader: eventLoader,
      },
    ],
  },
]);

basename

The basename of the app for situations where you can’t deploy to the root of the domain, but a sub directory.

createBrowserRouter(routes, {
  basename: "/app",
})

The trailing slash will be respected when linking to the root:

createBrowserRouter(routes, {
  basename: "/app",
});
<Link to="/" />; // results in <a href="/app" />
 
createBrowserRouter(routes, {
  basename: "/app/",
});
<Link to="/" />; // results in <a href="/app/" />

future

An optional set of Future Flags to enable for this Router. We recommend opting into newly released future flags sooner rather than later to ease your eventual migration to v7.

const router = createBrowserRouter(routes, {
  future: {
    // Normalize `useNavigation()`/`useFetcher()` `formMethod` to uppercase
    v7_normalizeFormMethod: true,
  },
})

window

Useful for environments like browser devtool plugins or testing to use a different window than the global window.

Route(s)

Routes are perhaps the most important part of a React Router app. They couple URL segments to components, data loading and data mutations. Through route nesting, complex application layouts and data dependencies become simple and declarative.

Routes are objects passed to the router creation functions:

const router = createBrowserRouter([
  {
    // it renders this element
    element: <Team />,
 
    // when the URL matches this segment
    path: "teams/:teamId",
 
    // with this data loaded before rendering
    loader: async ({ request, params }) => {
      return fetch(``/fake/api/teams/${params.teamId}.json``,
        { signal: request.signal }
      );
    },
 
    // performing this mutation when data is submitted to it
    action: async ({ request }) => {
      return updateFakeTeam(await request.formData());
    },
 
    // and renders this element in case something went wrong
    errorElement: <ErrorBoundary />,
  },
]);

You can also declare your routes with JSX and createRoutesFromElements, the props to the element are identical to the properties of the route objects:

 const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      element={<Team />}
      path="teams/:teamId"
      loader={async ({ params }) => {
        return fetch(
          `/fake/api/teams/${params.teamId}.json`
        );
      }}
      action={async ({ request }) => {
        return updateFakeTeam(await request.formData());
      }}
      errorElement={<ErrorBoundary />}
    />
  )
);

When using RouterProvider, if you do not wish to specify a React element (i.e., element={<MyComponent />}) you may specify a Component instead (i.e., Component={MyComponent}) and React Router will call createElement for you internally.

You should only do this for RouterProvider applications though since using Component inside of <Routes> will de-optimize React’s ability to reuse the created element across renders.

Type Declaration

interface RouteObject {
  path?: string
  index?: boolean
  children?: React.ReactNode
  caseSensitive?: boolean
  id?: string
  loader?: LoaderFunction
  action?: ActionFunction
  element?: React.ReactNode | null
  Component?: React.ComponentType | null
  errorElement?: React.ReactNode | null
  ErrorBoundary?: React.ComponentType | null
  handle?: RouteObject["handle"]
  shouldRevalidate?: ShouldRevalidateFunction
  lazy?: LazyRouteFunction<RouteObject>
}

Deep dive

path

The path pattern to match against the URL to determine if this route matches a URL, link href, or form action.

URL Segments

Dynamic Segment

If a path segment starts with : then it becomes a “dynamic segment”. When the route matches the URL, the dynamic segment will be parsed from the URL and provided as params to other router APIs.

<Route
  // this path will match URLs like
  // - /teams/hotspur
  // - /teams/real
  path="/teams/:teamId"
  // the matching param will be available to the loader
  loader={({ params }) => {
    console.log(params.teamId); // "hotspur"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Team />}
/>;
 
// and the element through `useParams`
function Team() {
  let params = useParams();
  console.log(params.teamId); // "hotspur"
}

You can have multiple dynamic segments in one route path:

<Route path="/c/:categoryId/p/:productId" />;
// both will be available
params.categoryId;
params.productId;

Dynamic segments cannot be "partial":

  • 🚫 "/teams-:teamId"
  • ✅ "/teams/:teamId"
  • 🚫 "/:category--:productId"
  • ✅ "/:productSlug"

Optional segments

You can make a route segment optional by adding a ? to the end of the segment.

<Route
  // this path will match URLs like
  // - /categories
  // - /en/categories
  // - /fr/categories
  path="/:lang?/categories"
  // the matching param might be available to the loader
  loader={({ params }) => {
    console.log(params["lang"]); // "en"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Categories />}
/>;
 
// and the element through `useParams`
function Categories() {
  let params = useParams();
  console.log(params.lang);
}

You can have optional static segments, too:

<Route path="/project/task?/:taskId" />

Splats

Also known as “catchall” and “star” segments. If a route path pattern ends with /* then it will match any characters following the /, including other / characters.

<Route
  // this path will match URLs like
  // - /files
  // - /files/one
  // - /files/one/two
  // - /files/one/two/three
  path="/files/*"
  // the matching param will be available to the loader
  loader={({ params }) => {
    console.log(params["*"]); // "one/two"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Team />}
/>;
 
// and the element through `useParams`
function Team() {
  let params = useParams();
  console.log(params["*"]); // "one/two"
}

You can destructure the *, you just have to assign it a new name. A common name is splat:

let { org, "*": splat } = params

Layout Routes

Omitting the path makes this route a "layout route". It participates in UI nesting, but it does not add any segments to the URL.

<Route
  element={
    <div>
      <h1>Layout</h1>
      <Outlet />
    </div>
  }
>
  <Route path="/" element={<h2>Home</h2>} />
  <Route path="/about" element={<h2>About</h2>} />

In this example, <h1>Layout</h1> will be rendered along with each child route’s element prop, via the layout route’s Outlet.

Outlet

An <Outlet> should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
 
      {/* This element will render either <DashboardMessages> when the URL is
          "/messages", <DashboardTasks> at "/tasks", or null if it is "/"
      */}
      <Outlet />
    </div>
  );
}
 
function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="messages"
          element={<DashboardMessages />}
        />
        <Route path="tasks" element={<DashboardTasks />} />
      </Route>
    </Routes>
  );
}

Index

Determines if the route is an index route. Index routes render into their parent’s Outlet at their parent’s URL (like a default child route).

<Route path="/teams" element={<Teams />}>
  <Route index element={<TeamsIndex />} />
  <Route path=":teamId" element={<Team />} />
</Route>

hildren

caseSensitive Route

Instructs the route to match case or not:

<Route caseSensitive path="/wEll-aCtuA11y" />
  • Will match "wEll-aCtuA11y"
  • Will not match "well-actua11y"

loader

Each route can define a “loader” function to provide data to the route element before it renders.

This feature only works if using a data router, see Picking a Router

<Route
  path="/teams/:teamId"
  loader={({ params }) => {
    return fetchTeam(params.teamId);
  }}
/>;
 
function Team() {
  let team = useLoaderData();
  // ...
}

If you are not using a data router like createBrowserRouter, this will do nothing

createBrowserRouter([
  {
    element: <Teams />,
    path: "teams",
    loader: async () => {
      return fakeDb.from("teams").select("*");
    },
    children: [
      {
        element: <Team />,
        path: ":teamId",
        loader: async ({ params }) => {
          return fetch(`/api/teams/${params.teamId}.json`);
        },
      },
    ],
  },
]);

As the user navigates around the app, the loaders for the next matching branch of routes will be called in parallel and their data made available to components through useLoaderData.

params

Route params are parsed from dynamic segments and passed to your loader. This is useful for figuring out which resource to load:

createBrowserRouter([
  {
    path: "/teams/:teamId",
    loader: ({ params }) => {
      return fakeGetTeam(params.teamId)
    },
  },
])

Note that the :teamId in the path is parsed as provided as params.teamId by the same name.

request

This is a Fetch Request instance being made to your application.

function loader({ request }) {}

The most common use case is creating a URL and reading the URLSearchParams from it:

function loader({ request }) {
  const url = new URL(request.url)
  const searchTerm = url.searchParams.get("q")
  return searchProducts(searchTerm)
}

Note that the APIs here are not React Router specific, but rather standard web objects: RequestURLURLSearchParams.

Returning Responses

While you can return anything you want from a loader and get access to it from useLoaderData, you can also return a web Response. This might not seem immediately useful, but consider fetch. Since the return value of fetch is a Response, and loaders understand responses, many loaders can return a simple fetch!

// an HTTP/REST API
function loader({ request }) {
  return fetch("/api/teams.json", {
    signal: request.signal,
  })
}
 
// or even a graphql endpoint
function loader({ request, params }) {
  return fetch("/_gql", {
    signal: request.signal,
    method: "post",
    body: JSON.stringify({
      query: gql`...`,
      params: params,
    }),
  })
}

You can construct the response yourself as well:

function loader({ request, params }) {
  const data = { some: "thing" }
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      "Content-Type": "application/json; utf-8",
    },
  })
}

React Router will automatically call response.json() so your components don’t need to parse it while rendering:

function SomeRoute() {
  const data = useLoaderData()
  // { some: "thing" }
}

Using the json utility simplifies this so you don’t have to construct them yourself. This next example is effectively the same as the previous example:

import { json } from "react-router-dom"
 
function loader({ request, params }) {
  const data = { some: "thing" }
  return json(data, { status: 200 })
}

If you’re planning an upgrade to Remix, returning responses from every loader will make the migration smoother.

Towing in Loaders

You can throw in your loader to break out of the current call stack (stop running the current code) and React Router will start over down the “error path”.

function loader({ request, params }) {
  const res = await fetch(`/api/properties/${params.id}`);
  if (res.status === 404) {
    throw new Response("Not Found", { status: 404 });
  }
  return res.json();
}

Basic routing

React Router provides components like BrowserRouter, Route, and Link to set up basic routing. Here’s a simple example:

import { BrowserRouter as Router, Route, Link } from "react-router-dom"
 
function App() {
  return (
    <Router>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>
 
      <Route path="/" exact component={Home} />
      <Route path="/about" component={About} />
    </Router>
  )
}

In this example:

  • BrowserRouter sets up the router.
  • Link is used for navigation.
  • Route specifies which component to render based on the URL.

Nested Routing <a name="nested-routing"></a>

Nested routing allows you to create complex layouts with multiple nested components. You can nest Route components within other components:

<Route path="/dashboard" component={Dashboard}>
  <Route path="/dashboard/profile" component={Profile} />
  <Route path="/dashboard/settings" component={Settings} />
</Route>

Programmatic Navigation <a name="programmatic-navigation"></a>

Sometimes, you need to navigate programmatically, e.g., after a form submission. You can use the useHistory hook for this:

Copy code
import { useHistory } from 'react-router-dom';
 
function MyComponent() {
  const history = useHistory();
 
  function handleButtonClick() {
    history.push('/new-page');
  }
}

URL parameter

  • useParams()
  • useSearchParams()
    • called in event handlers or effect
  • useLocation()

Route Parameters

You can capture dynamic values from the URL using route parameters. For instance, to create a profile page with a dynamic username

<Route path="/profile/:username" component={Profile} />

Access the parameter in your component using useParams hook:

import { useParams } from "react-router-dom"
function Profile() {
  const { username } = useParams()
  return <div>{username}'s Profile</div>
}

NavLink & Link

React Router has a component called NavLink. It is similar to Link but is mainly used when dealing with menu navigation links, unlike the Link component, which can be used for any type of link.

The major difference between NavLink and Link is that NavLink can detect when it is in an active state. When NavLink detects that it is active, it adds an active class to its component by default.

Since navigation menus are present in the history website, let’s update the Link component to NavLink :

import { Link, NavLink } from "react-router-dom"
 
function Nav() {
  return (
    <ul className="nav">
      <li>
        <NavLink to="/">Home</NavLink>
      </li>
      <li>
        <NavLink to="/eras">Eras</NavLink>
      </li>
    </ul>
  )
}

Overall, NavLink is a more robust option than the Link component when creating links for navigation menus.

Basic routing

React Router provides components like BrowserRouter, Route, and Link to set up basic routing. Here’s a simple example:

import { BrowserRouter as Router, Route, Link } from "react-router-dom"
 
function App() {
  return (
    <Router>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>
 
      <Route path="/" exact component={Home} />
      <Route path="/about" component={About} />
    </Router>
  )
}

In this example:

  • BrowserRouter sets up the router.
  • Link is used for navigation.
  • Route specifies which component to render based on the URL.

Route Parameters

You can capture dynamic values from the URL using route parameters. For instance, to create a profile page with a dynamic username

<Route path="/profile/:username" component={Profile} />

Access the parameter in your component using useParams hook:

import { useParams } from "react-router-dom"
function Profile() {
  const { username } = useParams()
  return <div>{username}'s Profile</div>
}

Nested Routing <a name="nested-routing"></a>

Nested routing allows you to create complex layouts with multiple nested components. You can nest Route components within other components:

<Route path="/dashboard" component={Dashboard}>
  <Route path="/dashboard/profile" component={Profile} />
  <Route path="/dashboard/settings" component={Settings} />
</Route>

Programmatic Navigation <a name="programmatic-navigation"></a>

Sometimes, you need to navigate programmatically, e.g., after a form submission. You can use the useHistory hook for this:

Copy code
import { useHistory } from 'react-router-dom';
 
function MyComponent() {
  const history = useHistory();
 
  function handleButtonClick() {
    history.push('/new-page');
  }
}

Optimistic Ui

Optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result.

Optimistic UI provides an easy way to make your UI respond much faster, while ensuring that the data becomes consistent with the actual response when it arrives.

Basic optimistic UI

Let’s say we have an “edit comment” mutation, and we want the UI to update immediately when the user submits the mutation, instead of waiting for the server response. This is what the optimisticResponse parameter to the mutate function provides.

The main way to get GraphQL data into your UI components with Apollo is to use a query, so if we want our optimistic response to update the UI, we have to make sure to return an optimistic response that will update the correct query result.

Loader

What is it ?

Loaders are a new feature in React Router v6 that allow you to load data for a route before it renders. This can be useful for fetching data from APIs, databases, or other external sources.

How does it work?

To use a loader, you simply add a loader prop to your route definition. The loader prop should be a function that returns a Promise. The Promise will be resolved with the data that you want to load for the route.

  • Export a loader function from the page that fetches the data that page will need
  • Pass loader prop to the route that renders that page and pass in the loader function
  • Use the useLoaderData hook in the component to get the data.

Once the loader function has resolved, the data will be made available to the route element through the useLoaderData hook. You can then use the data in your route element as needed.

const router = createBrowserRouter([
  {
    path: "/teams",
    loader: async () => {
      // Fetch data from an API
      const response = await fetch("/api/teams")
      const teams = await response.json()
 
      return teams
    },
    element: <Teams />,
  },
])

MUST ORDER

The component is a core feature of React Router. It’s used for programmatic navigation within your application. With it, you can dynamically change the route without requiring user interaction. It’s part of React Router’s declarative and component-based approach to routing.

Use Cases

Basic Usage

The Navigate component is typically used within your route configuration to specify where to navigate when a particular route is matched. It can be employed within the <Route> component to control the routing behavior. Here’s a basic example:

import { Route, Routes, Navigate } from 'react-router-dom';
 
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />} />
      <Route path="about" element={<About />} />
      <Route path="contact" element={<Contact />} />
      <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  );
}
 

In this example, if no matching route is found, the <Navigate to="/" /> component will redirect users to the home page.

Redirects

You can use <Navigate /> to redirect users from one route to another, such as after a form submission.

<Navigate to="/dashboard" />

Conditional Navigation

Conditionally navigate users based on certain criteria, like user authentication.

const isAuthenticated = /* check user's authentication */;
return isAuthenticated ? <Navigate to="/dashboard" /> : <Navigate to="/login" />;

Replace prop with navigation

<Route
  path="profile/:id"
  element={isAuthenticated ? <UserProfile /> : <Navigate to="/login" replace />}
/>

The replace prop, as shown in the example above, allows you to replace the current history entry when navigating. This means that the user won't be able to use the browser's back button to return to the previous page. It can be useful for scenarios where you want to prevent navigation history from being exposed.

Opinions

  • 🔥 Opinion:
    • ➡️ The Navigate component is a clean and declarative way to handle navigation within your application, making your routing logic more self-explanatory and maintainable.

Gotcha

  • 🔥 Gotcha:
    • ➡️ Ensure that the Navigate component is placed within a <Routes> component and that it is conditionally used within a route’s element prop. Placing it outside of the routing context may not produce the desired navigation behavior.

Use Navigation (useNavigation)

This hook tells you everything you need to know about a page navigation to build pending navigation indicators and optimistic UI on data mutations. Things like:

  • ➡️ Global loading indicators
  • ➡️ Disabling forms while a mutation is happening
  • ➡️ Adding busy indicators to submit buttons
  • ➡️ Optimistically showing a new record while it’s being created on the server
  • ➡️ Optimistically showing the new state of a record while it’s being updated

This feature only works if using a data router

import { useNavigation } from "react-router-dom"
 
function SomeComponent() {
  const navigation = useNavigation()
  navigation.state
  navigation.location
  navigation.formData
  navigation.json
  navigation.text
  navigation.formAction
  navigation.formMethod
}
  • 🔥 The navigation state can be in one of the following state.
    • ➡️ idle - There is no navigation pending.
    • ➡️ submitting - A route action is being called due to a form submission using POST, PUT, PATCH, or DELETE
    • ➡️ loading - The loaders for the next routes are being called to render the next page

Normal navigations and GET form submissions transition through these states:

idle → loading → idle

Form submissions with POST, PUT, PATCH, or DELETE transition through these states:

idle → submitting → loading → idle

function SubmitButton() {
  const navigation = useNavigation();
  const text = navigation.state === "submitting"
	      : navigation.state === "loading"
	      ? "Saved!" : "Go";
  return <button type="submit">{text}</button>;
}

Any POST, PUT, PATCH, or DELETE navigation that started from a <Form> or useSubmit will have your form’s submission data attached to it. This is primarily useful to build “Optimistic UI” with the submission.formData FormData object.

In the case of a GET form submission, formData will be empty and the data will be reflected in navigation.location.search.

This tells you what the next location is going to be.

Note that this link will not appear “pending” if a form is being submitted to the URL the link points to, because we only do this for “loading” states. The form will contain the pending UI for when the state is “submitting”, once the action is complete, then the link will go pending.