Mastering React Router v7 Handles: Breadcrumbs, Actions & Dynamic Data

February 26, 2025

react router handles webpage with person

What Are Handles?

Handles are essentially custom metadata that you attach directly to your route definitions. They’re not used by the router to perform matching or navigation. Instead, they serve as a lightweight mechanism to pass extra, user-defined data—such as breadcrumb labels, header actions, or even custom logic—to your layout components. This separation means you can enrich your UI without interfering with the core routing logic. In other words, handles let you define route-specific configuration in a clean and decoupled way.

Official Documentation

For more details on how handles work and their intended usage, you can check out the official React Router documentation here. Yeah. It does not say much. There is a basic breadcrumbs guide on the Remix (practically React Router + Vite plugin) documentation pages.

How Do I Attach Metadata To A Route?

If you're using React Router v7 in framework mode, the code compilation process enables you to simply export a handle object within the file that defines the entrypoint for your route.

export const handle = {
	breadcrumb: 'About me',
};

export default function AboutPage() {
	return <div>{/* Inspiring about page content */}</div>;
}

It's as easy as that. For the first simple example, let's imagine we add a breadcrumb text to the handle, even though there is more to handles that we can explore later.

Using Handles in a Layout Component

One of the most powerful applications of handles is within layout components. By using the useMatches hook, you can access an array of all the currently matched routes along with their respective metadata—including any handles you’ve defined as well as the loader data. This enables you to build features like breadcrumbs. The layout component can iterate through each matched route, inspect its handle, and render UI elements accordingly.

Let's imagine that we have a default page layout, within which we render a header, that should have breadcrumbs in it, depicting the route hierarchy from the root to the current page. We should define a component that renders something like that.

Example code snippet for a breadcrumb layout component

import { Fragment } from 'react/jsx-runtime';
import { useMatches } from 'react-router';
import {
	Breadcrumb,
	BreadcrumbList,
	BreadcrumbItem,
	BreadcrumbLink,
	BreadcrumbSeparator,
} from '../ui/breadcrumb';

export function AppBreadcrumbs() {
	const matches = useMatches().filter(
		({ handle }) => handle?.breadcrumb,
	);

	const relevantMatches = matches.filter((match) => match.handle.breadcrumb !== undefined);

	return (
		<Breadcrumb>
			<BreadcrumbList>
				{relevantMatches.map((match, i) => (
					<Fragment key={i}>
						<BreadcrumbItem className="hidden md:block" key={i}>
							<BreadcrumbLink asChild>{match.handle.breadcrumb}</BreadcrumbLink>
						</BreadcrumbItem>
						<BreadcrumbSeparator className="hidden md:block" />
					</Fragment>
				))}
			</BreadcrumbList>
		</Breadcrumb>
	);
}

The snippet above is a simple JSX example of how we can leverage the useMatches hook to get all of the route metadata for the current route, along with the handle data associated to them. If any route has a breadcrumb handle, we append it to the list of breadcrumbs, making them additive and render in the order of matching. We'll add some typing later, once we understand the core ideas.

Handling Dynamic Data in Handles

For pages that rely on dynamic data, such as a details page for a blog post, handles will need to access the fetched data for the current route such as the name of the blog post. So, let's imagine a blog post detail page: the loader fetches the blog post data, and the handle ideally leverages this information to generate a dynamic breadcrumb title (e.g., the title of the blog post). That is of course possible in React Router, since handles can include any value, even a component. If the loader data updates, the layout component’s call to useMatches will provide the new handle value, and the UI will automatically update.

Example code snippet with dynamic data rendering

export function AppBreadcrumbs() {
	const matches = useMatches().filter(
		({ handle }) => handle?.breadcrumb,
	);

	const relevantMatches = matches.filter((match) => match.handle.breadcrumb !== undefined);

	return (
		<Breadcrumb>
			<BreadcrumbList>
				{relevantMatches.map((match, i) => (
					<Fragment key={i}>
						<BreadcrumbItem className="hidden md:block" key={i}>
							<BreadcrumbLink asChild>{match.handle.breadcrumb?.(match.data)}</BreadcrumbLink>
						</BreadcrumbItem>
						<BreadcrumbSeparator className="hidden md:block" />
					</Fragment>
				))}
			</BreadcrumbList>
		</Breadcrumb>
	);
}

The tweak that we needed to do here is to call the breadcrumb as if it were a component and pass it the match.data value, which holds the loader data in it.

The next thing we need to do is to tweak the handle itself to accept the loader data.

Example code snippet for the handle as a component

function Breadcrumb({ blog }) {
	return <Link to={`/blog/${blog.id}`}>{blog.title}</Link>;
}

export const handle = {
	breadcrumb: Breadcrumb,
};

As long as the loader data, found in match.data contains the blog, this should work.

Adding type safety

The nice thing about React Router running in framework mode is that it provides you a typegen watcher out of the box when developing locally. As long as you're running an npm run dev process, your data loader typing should be automatically generated on the fly as you change the loaders.

To add typing to your breadcrumb component, you can leverage the generated type.

Example code snippet for the typed handle

import type { Route } from './+types/blog-show';

// This would ideally be placed in a separate types file
export type BreadcrumbHandle = { breadcrumb: (data?: any) => JSX.Element };

function Breadcrumb({ blog }: Route.ComponentProps['loaderData']) {
	return <Link to={`/blog/${blog.id}`}>{blog.title}</Link>;
}

export const handle = {
	breadcrumb: Breadcrumb,
};

What remains is typing the layout breadcrumb generating component.

Example code snippet for the typed Breadcrumb renderer

// This would ideally be placed in a separate types file
type BreadcrumbHandle = { breadcrumb: (data?: any) => JSX.Element };
type BreadcrumbMatch = UIMatch<Record<string, unknown>, BreadcrumbHandle>;

const isBreadcrumbMatch = (match: unknown): match is BreadcrumbMatch =>
	typeof match === 'object' &&
	match !== null &&
	'handle' in match &&
	typeof match.handle === 'object' &&
	match.handle !== null &&
	'breadcrumb' in match.handle &&
	typeof match.handle.breadcrumb == 'function';

export function AppBreadcrumbs() {
	const matches = useMatches();
	const breadcrumbMatches = matches.filter(isBreadcrumbMatch);

	return (
		<Breadcrumb>
			<BreadcrumbList>
				{breadcrumbMatches.map((match, i) => (
					<Fragment key={i}>
						<BreadcrumbItem className="hidden md:block" key={i}>
							<BreadcrumbLink asChild>{match.handle.breadcrumb(match.data)}</BreadcrumbLink>
						</BreadcrumbItem>
						<BreadcrumbSeparator className="hidden md:block" />
					</Fragment>
				))}
			</BreadcrumbList>
		</Breadcrumb>
	);
}

The typeguard is not exact, since there is no really elegant way to validate that something is a valid React node, but it will do for the majority of use cases you'll have.

Use Cases and Examples

Handles can be applied in many scenarios:

  • Dynamic Breadcrumbs: As shown above, each route can contribute its own breadcrumb label, which the layout aggregates into a navigation trail.
  • Dynamic Header Actions: Routes can also define header actions (like a “Create New” button) that change depending on the current page context and can be used to inject any content into a layout anywhere outside of the current route component, providing a portal-like developer experience.
  • Any Other Contextual UI Component, really: Beyond breadcrumbs and actions, handles can provide any kind of route-specific metadata that your components might use, such as rendering a page title somewhere in the header.

Summary

In this post, we’ve explored how to leverage the handle mechanism in React Router v7. We learned that:

  • Handles are custom metadata attached to route definitions.
  • They empower layout components to dynamically render UI elements like breadcrumbs and header actions.
  • The useMatches hook provides an array of matched routes, including their handles.
  • Dynamic data can be seamlessly integrated, ensuring your UI reflects real-time changes.
  • Finally, by typing handle data, you can improve type safety and developer experience.
  • This flexible approach not only introduces some sort of concensus for the management of UI metadata but also keeps your routing and layouting logic clean.

p.s. Yes, the image that DALL-E has generated for this blog is absolutely hilarious, and I am keeping it in.