Documentation Index Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
handle
An arbitrary object associated with a route that can be accessed by parent routes and components. Useful for breadcrumbs, navigation, permissions, and other route-level metadata.
Signature
export const handle = {
// Any data you want
};
The handle can be any value, but is typically an object containing metadata about the route.
Basic Example
// app/routes/projects.$id.tsx
export const handle = {
breadcrumb: "Project Details" ,
};
export default function ProjectDetails () {
return < div > { /* route content */ } </ div > ;
}
Accessing Handle in Components
import { useMatches } from "react-router" ;
export default function Breadcrumbs () {
const matches = useMatches ();
return (
< nav >
< ol >
{ matches
. filter (( match ) => match . handle ?. breadcrumb )
. map (( match ) => (
< li key = { match . pathname } >
< Link to = { match . pathname } >
{ match . handle . breadcrumb }
</ Link >
</ li >
)) }
</ ol >
</ nav >
);
}
Dynamic Breadcrumbs with Data
// app/routes/projects.$id.tsx
export async function loader ({ params } : Route . LoaderArgs ) {
const project = await fetchProject ( params . id );
return { project };
}
export const handle = {
breadcrumb : ( match : any ) => match . data . project . name ,
};
// app/root.tsx or layout component
import { useMatches } from "react-router" ;
export default function Layout () {
const matches = useMatches ();
return (
< div >
< nav >
< ol >
{ matches
. filter (( match ) => match . handle ?. breadcrumb )
. map (( match ) => {
const breadcrumb = typeof match . handle . breadcrumb === "function"
? match . handle . breadcrumb ( match )
: match . handle . breadcrumb ;
return (
< li key = { match . pathname } >
< Link to = { match . pathname } > { breadcrumb } </ Link >
</ li >
);
}) }
</ ol >
</ nav >
< main >
< Outlet />
</ main >
</ div >
);
}
// app/routes/dashboard.tsx
export const handle = {
nav: {
label: "Dashboard" ,
icon: "📊" ,
order: 1 ,
},
};
// app/routes/settings.tsx
export const handle = {
nav: {
label: "Settings" ,
icon: "⚙️" ,
order: 2 ,
},
};
// Navigation component
import { useMatches } from "react-router" ;
export default function Navigation () {
const matches = useMatches ();
const navItems = matches
. filter (( match ) => match . handle ?. nav )
. map (( match ) => match . handle . nav )
. sort (( a , b ) => a . order - b . order );
return (
< nav >
{ navItems . map (( item ) => (
< Link key = { item . label } to = { item . path } >
< span > { item . icon } </ span >
< span > { item . label } </ span >
</ Link >
)) }
</ nav >
);
}
Permissions and Access Control
// app/routes/admin.tsx
export const handle = {
permissions: [ "admin" ],
};
// app/routes/admin.users.tsx
export const handle = {
permissions: [ "admin" , "user-management" ],
};
// Authorization component
import { useMatches , useNavigate } from "react-router" ;
export function useRequirePermissions () {
const matches = useMatches ();
const navigate = useNavigate ();
const user = useUser ();
useEffect (() => {
const requiredPermissions = matches
. filter (( match ) => match . handle ?. permissions )
. flatMap (( match ) => match . handle . permissions );
const hasPermission = requiredPermissions . every (( permission ) =>
user . permissions . includes ( permission )
);
if ( ! hasPermission ) {
navigate ( "/unauthorized" );
}
}, [ matches , user , navigate ]);
}
Layout Variants
// app/routes/auth.login.tsx
export const handle = {
layout: "centered" ,
};
// app/routes/dashboard.tsx
export const handle = {
layout: "sidebar" ,
};
// app/root.tsx
import { useMatches } from "react-router" ;
export default function Root () {
const matches = useMatches ();
// Get layout from deepest matching route
const layout = matches
. reverse ()
. find (( match ) => match . handle ?. layout )?. handle . layout || "default" ;
return (
< html >
< body >
{ layout === "centered" && < CenteredLayout /> }
{ layout === "sidebar" && < SidebarLayout /> }
{ layout === "default" && < DefaultLayout /> }
</ body >
</ html >
);
}
I18n and Localization
// app/routes/products.$id.tsx
export const handle = {
i18n: [ "products" , "common" ],
};
// Load translations based on handles
import { useMatches } from "react-router" ;
export function useI18n () {
const matches = useMatches ();
const namespaces = [ ... new Set (
matches
. filter (( match ) => match . handle ?. i18n )
. flatMap (( match ) => match . handle . i18n )
)];
return useTranslation ( namespaces );
}
Analytics and Tracking
// app/routes/products.tsx
export const handle = {
analytics: {
category: "products" ,
label: "Product List" ,
},
};
// Track page views
import { useMatches , useLocation } from "react-router" ;
export function usePageTracking () {
const location = useLocation ();
const matches = useMatches ();
useEffect (() => {
const currentMatch = matches [ matches . length - 1 ];
const analytics = currentMatch ?. handle ?. analytics ;
if ( analytics ) {
trackPageView ({
path: location . pathname ,
category: analytics . category ,
label: analytics . label ,
});
}
}, [ location , matches ]);
}
// app/routes/docs.$slug.tsx
export const handle = {
scrollMode: "top" , // scroll to top on navigation
};
// app/routes/search.tsx
export const handle = {
scrollMode: "preserve" , // maintain scroll position
};
// Custom scroll behavior
import { useMatches , useLocation } from "react-router" ;
export function useScrollBehavior () {
const location = useLocation ();
const matches = useMatches ();
useEffect (() => {
const currentMatch = matches [ matches . length - 1 ];
const scrollMode = currentMatch ?. handle ?. scrollMode || "auto" ;
if ( scrollMode === "top" ) {
window . scrollTo ( 0 , 0 );
}
// "preserve" means do nothing
}, [ location , matches ]);
}
TypeScript Types
// Define a type for your handle
type RouteHandle = {
breadcrumb ?: string | (( match : any ) => string );
nav ?: {
label : string ;
icon : string ;
order : number ;
};
permissions ?: string [];
layout ?: "default" | "centered" | "sidebar" ;
};
// Use in route
export const handle : RouteHandle = {
breadcrumb: "Dashboard" ,
nav: {
label: "Dashboard" ,
icon: "📊" ,
order: 1 ,
},
};
// Access in components
import { useMatches } from "react-router" ;
type RouteMatch = ReturnType < typeof useMatches >[ number ] & {
handle ?: RouteHandle ;
};
export function Breadcrumbs () {
const matches = useMatches () as RouteMatch [];
// Now handle is typed
}
Best Practices
Use handle for route-level metadata only
Avoid functions and complex objects when possible: // ✅ Simple, serializable
export const handle = {
title: "Products" ,
category: "catalog" ,
};
// ⚠️ Works but harder to debug
export const handle = {
title : ( match ) => match . data . product . name ,
validator : ( data ) => validateData ( data ),
};
Use TypeScript for type safety
Define a shared type for your handle objects: // types/route.ts
export type AppRouteHandle = {
breadcrumb ?: string | (( match : any ) => string );
permissions ?: string [];
layout ?: "default" | "centered" ;
};
// routes/products.tsx
import type { AppRouteHandle } from "~/types/route" ;
export const handle : AppRouteHandle = {
breadcrumb: "Products" ,
permissions: [ "view:products" ],
};
Use useMatches() to access handle data: import { useMatches } from "react-router" ;
export function Component () {
const matches = useMatches ();
// Get current route's handle
const currentHandle = matches [ matches . length - 1 ]?. handle ;
// Get all handles
const allHandles = matches . map (( match ) => match . handle );
// Find specific handle
const rootHandle = matches . find (( m ) => m . id === "root" )?. handle ;
}
Common Use Cases
Breadcrumbs : Show page hierarchy
Navigation : Build dynamic nav from route metadata
Permissions : Check access control requirements
Analytics : Track page categories and labels
Layout : Choose layout variant per route
I18n : Determine which translations to load
Scroll : Control scroll restoration behavior
SEO : Add route-specific meta information
See Also