React Router Cheatsheet

Code Splitting and Prefetching

Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Code Splitting

// Data mode: `lazy` on the route
{
  path: "reports",
  lazy: async () => {
    const mod = await import("./routes/reports");
    return { Component: mod.default, loader: mod.loader };
  },
}

lazy splits the loader too, which React.lazy cannot do. That matters, because a React.lazy route still has to download the component before the fetch can start, reintroducing the waterfall.

// Declarative mode: React.lazy plus Suspense
const Reports = React.lazy(() => import("./Reports"));

<Route
  path="reports"
  element={
    <Suspense fallback={<Skeleton />}>
      <Reports />
    </Suspense>
  }
/>

Framework mode splits every route automatically, so there is nothing to configure.

Prefetching

<Link to="/reports" prefetch="intent">Reports</Link>
ValueFetches when
noneNever (the default)
intentOn hover or focus
renderAs soon as the link renders
viewportWhen the link scrolls into view

intent is the right default for most navigation: by the time a user finishes moving the mouse and clicking, the data and the code have already arrived.

Measuring the Split

npx vite-bundle-visualizer          # what ended up in each chunk
npm run build -- --sourcemap

In framework mode the build prints a per-route chunk table, which is the fastest way to spot a route that accidentally imports a heavy dependency at the module level.

SymptomCause
One giant chunkA shared module imports every route eagerly
A route chunk pulls in a chart libraryMove the import inside the component, or lazy it
Loader code in the main bundleUse route lazy, not React.lazy

Lazy Route Modules

// routes/reports.jsx exports what the router needs
export async function loader({ request }) { /* … */ }
export default function Reports() { /* … */ }
export function ErrorBoundary() { /* … */ }
{
  path: "reports",
  lazy: () => import("./routes/reports").then((m) => ({
    loader: m.loader,
    Component: m.default,
    ErrorBoundary: m.ErrorBoundary,
  })),
}

Keep path, index, and children static on the route object. Only the behavior (component, loader, action, boundary) can be lazy, because the router needs the shape of the tree to match a URL before it decides what to download.

Preloading Assets

// Framework mode: a links export becomes real <link> tags
export function links() {
  return [
    { rel: "preload", href: "/fonts/inter.woff2", as: "font", type: "font/woff2", crossOrigin: "anonymous" },
    { rel: "preload", href: heroImage, as: "image" },
    { rel: "stylesheet", href: styles },
  ];
}