React Router Cheatsheet
Routes and Nesting
Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The Route Config
Framework mode routes are declared in app/routes.ts with helpers from
@react-router/dev/routes. Paths and file paths are separate, so a URL
never has to match a folder name.
| Helper | Signature | What it makes |
|---|---|---|
index | index(file) | A child that matches the parent path exactly |
route | route(path, file, children?) | A path segment with a module |
layout | layout(file, children) | A pathless parent that renders <Outlet /> |
prefix | prefix(path, children) | Prepends a segment, adds no module |
// app/routes.ts import { type RouteConfig, index, route, layout, prefix, } from "@react-router/dev/routes"; export default [ index("routes/home.tsx"), route("about", "routes/about.tsx"), layout("routes/dashboard/layout.tsx", [ route("dashboard", "routes/dashboard/index.tsx"), route("dashboard/settings", "routes/dashboard/settings.tsx"), ]), ...prefix("concerts", [ index("routes/concerts/home.tsx"), route(":city", "routes/concerts/city.tsx"), route("trending", "routes/concerts/trending.tsx"), ]), route("*", "routes/not-found.tsx"), ] satisfies RouteConfig;
Pass route(path, file, [children]) to nest under a path, and use
layout when the parent adds UI but no URL segment. relative(dir)
returns helpers whose file paths resolve against dir, which is how a
large route config splits across several files.
Path Syntax
| Pattern | Matches | params |
|---|---|---|
teams/:teamId | /teams/42 | teamId: "42" |
:lang?/about | /about, /en/about | lang?: string |
files/* | /files/a/b.txt | "*": "a/b.txt" |
index(f) | The parent path exactly | — |
* | Everything unmatched, put it last | "*" |
:id.pdf | /42.pdf | id: "42" |
Matching is ranked, not first wins: a static segment always beats a dynamic one at the same depth, so ordering in the array does not decide which route wins. Params are always strings, so parse and validate them.
// params are typed from the path, no cast needed import type { Route } from "./+types/city"; export async function loader({ params }: Route.LoaderArgs) { return { shows: await getShows(params.city) }; } export default function City({ params }: Route.ComponentProps) { return <h1>{params.city}</h1>; }
useParams() still works anywhere in the tree, but inside a route module
the params prop is already typed and is the better read.
Nesting and Outlet
A parent route stays mounted while children change. The parent renders
<Outlet /> where the child goes, and both loaders run in parallel.
// routes/dashboard/layout.tsx import { Outlet, NavLink } from "react-router"; import type { Route } from "./+types/layout"; export async function loader() { return { user: await getUser() }; } export default function Layout({ loaderData }: Route.ComponentProps) { return ( <div className="grid"> <nav> <NavLink to="/dashboard" end>Overview</NavLink> <NavLink to="/dashboard/settings">Settings</NavLink> </nav> <main> <Outlet /> </main> </div> ); }
Nothing renders in a nested route if the parent forgets <Outlet />.
That is the single most common "my route is blank" cause.
File Based Routes
If you prefer conventions over a config array, @react-router/fs-routes
reads app/routes/ and produces the same config. Dots in a filename
become slashes.
// app/routes.ts import { type RouteConfig } from "@react-router/dev/routes"; import { flatRoutes } from "@react-router/fs-routes"; export default flatRoutes() satisfies RouteConfig;
| File | URL |
|---|---|
routes/_index.tsx | / |
routes/about.tsx | /about |
routes/concerts.$city.tsx | /concerts/:city |
routes/concerts._index.tsx | /concerts |
routes/concerts.tsx | Layout for the two above |
routes/_auth.login.tsx | /login, inside the _auth layout |
routes/files.$.tsx | /files/* |
routes/sitemap[.]xml.tsx | /sitemap.xml |
You can mix the two: flatRoutes() returns an array, so spread it next
to hand written route() entries.
Data and Declarative Routes
Outside Framework mode routes are objects or JSX. Same matching rules,
different declaration site, and Component replaces the default export.
// Data mode import { createBrowserRouter } from "react-router"; import { RouterProvider } from "react-router/dom"; const router = createBrowserRouter([ { path: "/", Component: Root, children: [ { index: true, Component: Home }, { path: "teams/:teamId", Component: Team, loader: teamLoader }, ], }, ]); createRoot(el).render(<RouterProvider router={router} />);
// Declarative mode import { BrowserRouter, Routes, Route } from "react-router"; <BrowserRouter> <Routes> <Route path="/" element={<Root />}> <Route index element={<Home />} /> <Route path="teams/:teamId" element={<Team />} /> </Route> </Routes> </BrowserRouter>
Declarative mode has no loaders, no actions, and no error boundaries per route. If you need any of those, you want Data or Framework mode.