React Router Cheatsheet

Framework Mode and SSR

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

The Setup

Framework mode is a Vite plugin plus two config files. The rest of this sheet assumes it is in place.

// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
});
// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;
CLIDoes
react-router devDev server with HMR and typegen in watch mode
react-router buildClient and server builds into build/
react-router-serve build/server/index.jsRun the production server
react-router typegenRegenerate ./+types/*, for CI before tsc
react-router routesPrint the route tree, --json for machine output
react-router revealWrite out entry.client.tsx and entry.server.tsx

Config Options

OptionDefaultWhat it does
ssrtruefalse prerenders a single index.html and gives you SPA mode
prerender—Array of URLs, or a function, or { paths, concurrency }
basename/Mount the app under a sub path
appDirectoryappWhere routes and root.tsx live
buildDirectorybuildOutput directory
splitRouteModulestruefalse to keep one chunk, "enforce" to require splitting
routeDiscovery{ mode: "lazy" }{ mode: "initial" } ships the whole manifest
allowedActionOrigins—Origin allowlist for action submissions, glob patterns allowed
presets—Platform presets, for example a deploy target
future{}Opt in to flags landing in the next major

The Root Route

app/root.tsx is the only route that is always matched. Its Layout export wraps both the app and any error boundary, which is why the document tags belong there.

// app/root.tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";
import "./app.css";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
ComponentRenders
<Meta />Every matched route's meta output
<Links />Every matched route's links output, plus route CSS
<Scripts />The hydration script. Omit it and the page stays static
<ScrollRestoration />Per history entry scroll position

Route Module Exports

ExportRunsPurpose
defaultClient and serverThe component, gets Route.ComponentProps
loaderServerData for the route
clientLoaderBrowserClient data, can wrap serverLoader()
actionServerHandles non GET submissions
clientActionBrowserClient mutation, can wrap serverAction()
middlewareServerRuns around the branch's loaders and actions
clientMiddlewareBrowserSame, client side
ErrorBoundaryClient and serverRenders thrown errors for this branch
HydrateFallbackBrowserShown while clientLoader runs on first load
headersServerResponse headers for the document
linksClient and server<link> descriptors, collected into <Links />
metaClient and serverMeta tags, collected into <Meta />
handleClient and serverArbitrary data read via useMatches()
shouldRevalidateBrowserOpt out of revalidation after a mutation
export function meta({ loaderData }: Route.MetaArgs) {
  return [{ title: loaderData.title }];
}

Since React 19 the built in <title>, <meta>, and <link> elements are recommended over the meta and links exports. Render them anywhere in the component and React hoists them into the head. Note that v8 removed the deprecated data field on MetaArgs, so read loaderData instead.

SSR, SPA, and Prerendering

// server render every request, the default
export default { ssr: true } satisfies Config;

// SPA mode, one index.html plus assets, no server
export default { ssr: false } satisfies Config;

// prerender specific paths to HTML at build time
export default {
  async prerender({ getStaticPaths }) {
    const posts = await getPostSlugs();
    return ["/", "/about", ...posts.map((s) => `/blog/${s}`)];
  },
} satisfies Config;

- With ssr: false only the root loader may run, and it runs at build time. Everything else must be a clientLoader. - Prerendered routes still hydrate and still do client routing, they just ship as static HTML. - Prerendering plus ssr: false gives a static site with real paths, which crawlers handle far better than a hash router.

Entry Files and Deployment

Both entry files are optional. react-router reveal writes them out when you need to customize hydration or the server response.

PackageTarget
@react-router/serveThe batteries included Node server
@react-router/nodeAny Node host, behind your own server
@react-router/expressAn Express app you already own
@react-router/cloudflareCloudflare Workers, with @cloudflare/vite-plugin
@react-router/architectAWS via Architect

Since v8.2 the default server entry picks its renderer from your dependencies. Apps with @react-router/{node,express,serve} use renderToPipeableStream, everything else uses renderToReadableStream, so non Node runtimes no longer need a custom entry.server.tsx. Node apps opt in with future.unstable_enableNodeReadableStream.

v8 removed the Cloudflare dev proxy at @react-router/dev/vite/cloudflare. Cloudflare projects use @cloudflare/vite-plugin instead.