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.

HelperSignatureWhat it makes
indexindex(file)A child that matches the parent path exactly
routeroute(path, file, children?)A path segment with a module
layoutlayout(file, children)A pathless parent that renders <Outlet />
prefixprefix(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

PatternMatchesparams
teams/:teamId/teams/42teamId: "42"
:lang?/about/about, /en/aboutlang?: string
files/*/files/a/b.txt"*": "a/b.txt"
index(f)The parent path exactly—
*Everything unmatched, put it last"*"
:id.pdf/42.pdfid: "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;
FileURL
routes/_index.tsx/
routes/about.tsx/about
routes/concerts.$city.tsx/concerts/:city
routes/concerts._index.tsx/concerts
routes/concerts.tsxLayout 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.