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;
| CLI | Does |
|---|---|
react-router dev | Dev server with HMR and typegen in watch mode |
react-router build | Client and server builds into build/ |
react-router-serve build/server/index.js | Run the production server |
react-router typegen | Regenerate ./+types/*, for CI before tsc |
react-router routes | Print the route tree, --json for machine output |
react-router reveal | Write out entry.client.tsx and entry.server.tsx |
Config Options
| Option | Default | What it does |
|---|---|---|
ssr | true | false 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 |
appDirectory | app | Where routes and root.tsx live |
buildDirectory | build | Output directory |
splitRouteModules | true | false 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 />; }
| Component | Renders |
|---|---|
<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
| Export | Runs | Purpose |
|---|---|---|
default | Client and server | The component, gets Route.ComponentProps |
loader | Server | Data for the route |
clientLoader | Browser | Client data, can wrap serverLoader() |
action | Server | Handles non GET submissions |
clientAction | Browser | Client mutation, can wrap serverAction() |
middleware | Server | Runs around the branch's loaders and actions |
clientMiddleware | Browser | Same, client side |
ErrorBoundary | Client and server | Renders thrown errors for this branch |
HydrateFallback | Browser | Shown while clientLoader runs on first load |
headers | Server | Response headers for the document |
links | Client and server | <link> descriptors, collected into <Links /> |
meta | Client and server | Meta tags, collected into <Meta /> |
handle | Client and server | Arbitrary data read via useMatches() |
shouldRevalidate | Browser | Opt 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.
| Package | Target |
|---|---|
@react-router/serve | The batteries included Node server |
@react-router/node | Any Node host, behind your own server |
@react-router/express | An Express app you already own |
@react-router/cloudflare | Cloudflare Workers, with @cloudflare/vite-plugin |
@react-router/architect | AWS 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-plugininstead.