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.
Lazy Route Discovery
Lazy route discovery enables you to load route definitions dynamically, reducing initial bundle size and improving performance.
Overview
React Router supports two levels of lazy loading:
- Lazy route modules (
route.lazy) - Load route properties (loader, component, etc.)
- Lazy route discovery (
patchRoutesOnNavigation) - Discover and add routes dynamically
Lazy Route Modules
Use lazy() to load route properties on demand:
const router = createBrowserRouter([
{
path: "/",
Component: Layout,
children: [
{
index: true,
Component: Home,
},
{
path: "about",
lazy: () => import("./routes/about"),
},
],
},
]);
Your lazy route module can export:
// routes/about.tsx
export async function loader() {
return { title: "About" };
}
export function Component() {
return <h1>About Page</h1>;
}
export function ErrorBoundary() {
return <div>Something went wrong!</div>;
}
Immutable Properties
Some properties cannot be loaded lazily:
path - Needed for matching
index - Needed for matching
caseSensitive - Needed for matching
id - Needed to identify the route
children - Needed for route hierarchy
These must be defined statically.
Static Properties
You can define properties statically and supplement with lazy loading:
{
path: "dashboard",
// Static loader runs in parallel with lazy()
loader: () => fetch("/api/dashboard"),
// Lazy load the component
lazy: () => import("./routes/dashboard"),
}
React Router optimizes by calling static loaders in parallel with lazy().
Route Discovery with patchRoutesOnNavigation
Discover and add routes dynamically during navigation:
const router = createBrowserRouter(
[
{
path: "/",
Component: Layout,
},
],
{
async patchRoutesOnNavigation({ path, patch }) {
if (path.startsWith("/dashboard")) {
const routes = await import("./routes/dashboard");
patch(null, routes.default);
}
},
}
);
The patch Function
patch(routeId: string | null, routes: RouteObject[])
routeId: Parent route ID, or null for root
routes: Array of routes to add
Eager Discovery (Framework Mode)
In framework mode with RSC, routes can be discovered eagerly:
<RSCHydratedRouter
payload={payload}
routeDiscovery="eager" // or "lazy"
createFromReadableStream={createFromReadableStream}
/>
Eager mode: Discovers routes as links render in the DOM
Lazy mode: Discovers routes only when clicked
Manifest Pattern
Load route manifests for efficient discovery:
let manifestCache = new Map();
async function patchRoutesOnNavigation({ path, patch }) {
if (manifestCache.has(path)) {
patch(null, manifestCache.get(path));
return;
}
const response = await fetch(`${path}.manifest`);
const routes = await response.json();
manifestCache.set(path, routes);
patch(null, routes);
}
Component vs element
Use Component with lazy routes instead of element:
// ❌ Don't do this
{
path: "about",
lazy: async () => ({
element: <About />, // Awkward JSX in lazy module
}),
}
// ✅ Do this
{
path: "about",
lazy: async () => ({
Component: About, // Clean component reference
}),
}
Interruptions
If a navigation is interrupted while lazy() is loading, React Router still calls the returned handler to maintain consistency:
// User clicks /about
lazy: () => import("./about") // starts loading
// User quickly clicks /contact (interrupts)
// The about loader still runs when loaded
// But the results are discarded
This ensures routes behave consistently on first and subsequent navigations.
SSR Hydration
When server-rendering with lazy routes, preload them before hydration:
// Determine initially matched lazy routes
const lazyMatches = matchRoutes(routes, window.location)?.filter(
(m) => m.route.lazy
);
// Load them before creating the router
if (lazyMatches?.length) {
await Promise.all(
lazyMatches.map(async (m) => {
const routeModule = await m.route.lazy!();
Object.assign(m.route, { ...routeModule, lazy: undefined });
})
);
}
const router = createBrowserRouter(routes, {
hydrationData: window.__hydrationData,
});
ReactDOM.hydrateRoot(
document.getElementById("app"),
<RouterProvider router={router} />
);
HMR Support
Lazy routes work seamlessly with Hot Module Replacement during development:
if (import.meta.hot) {
import.meta.hot.accept("./routes/about", () => {
// Route automatically updates
});
}
Benefits
- Smaller bundles: Only load code when needed
- Faster initial load: Reduce time to interactive
- Code splitting: Automatic via dynamic imports
- Progressive enhancement: Routes work before JS loads (with SSR)
Best Practices
- Lazy load large sections: Dashboard, admin, settings
- Keep critical routes static: Home, 404, layout
- Use route.lazy for components: Avoid lazy() for just loaders
- Preload on hover: Improve perceived performance
- Cache manifests: Don’t re-fetch route definitions
Example: Feature Flags
async function patchRoutesOnNavigation({ path, patch }) {
const features = await getFeatureFlags();
if (path === "/beta" && features.betaAccess) {
const routes = await import("./routes/beta");
patch(null, routes.default);
}
}