React Router Cheatsheet
Loaders
Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Loaders
A loader runs before the route renders, so the component never has a loading state for its own primary data. This eliminates the fetch-in-useEffect waterfall, where each nested component starts fetching only after its parent has rendered.
// Data mode const router = createBrowserRouter([ { path: "users/:id", element: <User />, loader: async ({ params, request }) => { const res = await fetch(`/api/users/${params.id}`, { signal: request.signal, }); if (!res.ok) throw new Response("Not found", { status: 404 }); return res.json(); }, }, ]); function User() { const user = useLoaderData(); // already here on first render return <h1>{user.name}</h1>; }
// Framework mode: one file per route import type { Route } from "./+types/user"; export async function loader({ params }: Route.LoaderArgs) { const user = await db.user.findUnique({ where: { id: params.id } }); if (!user) throw new Response("Not found", { status: 404 }); return { user }; } export default function User({ loaderData }: Route.ComponentProps) { return <h1>{loaderData.user.name}</h1>; // fully typed }
Loaders for a whole branch run in parallel, not in sequence, which is the other half of the waterfall fix.
Loader Arguments
| Argument | Is |
|---|---|
params | The matched dynamic segments |
request | A standard Request, so request.url and request.signal work |
context | Server-only value from your adapter (framework mode) |
loader: async ({ request, params }) => { const url = new URL(request.url); const page = Number(url.searchParams.get("page") ?? 1); const q = url.searchParams.get("q") ?? ""; return getUsers({ page, q, signal: request.signal }); }
Reading search params from request.url is how a loader reacts to ?q= changes: changing a search param re-runs the loader.
Returning and Throwing
// Return plain data (v7 serializes it for you) return { user, posts }; // Return a Response when you need headers or a status return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "max-age=60" }, }); // Redirect import { redirect, redirectDocument, replace } from "react-router"; throw redirect("/login"); throw redirect("/login", { status: 303 }); throw redirect(`/users/${id}`, { headers: { "Set-Cookie": cookie } }); throw redirectDocument("/legacy"); // full document load throw replace("/login"); // redirect without a history entry // Throw to the nearest error boundary throw new Response("Forbidden", { status: 403, statusText: "Forbidden" }); throw new Error("Something broke"); // Data with a status (replaces the old json() helper) import { data } from "react-router"; return data({ errors }, { status: 400 });
json() and defer() are deprecated in v7. Return raw objects, and use data() when you need to attach a status or headers.
Reading Loader Data
import { useLoaderData, useRouteLoaderData, useMatches } from "react-router"; const data = useLoaderData(); // this route's loader const root = useRouteLoaderData("root"); // another route's, by id
// Give a route an id so descendants can read its data { id: "root", path: "/", loader: rootLoader, children: [...] }
useRouteLoaderData is the idiomatic replacement for a context provider around the current user: load it once in the root loader, read it anywhere.
Streaming
Return a promise from a loader and render it with <Await> to send the shell immediately and stream the slow part in.
import { Suspense } from "react"; import { Await, useLoaderData } from "react-router"; export async function loader() { return { user: await getUser(), // awaited: blocks the response reviews: getReviews(), // not awaited: streams later }; } export default function Product() { const { user, reviews } = useLoaderData(); return ( <> <h1>{user.name}</h1> <Suspense fallback={<p>Loading reviews…</p>}> <Await resolve={reviews} errorElement={<p>Could not load reviews</p>}> {(list) => <ReviewList reviews={list} />} </Await> </Suspense> </> ); }
Streaming requires SSR and a streaming-capable host. In v7 you return bare promises, and the old defer() wrapper is no longer needed.