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

ArgumentIs
paramsThe matched dynamic segments
requestA standard Request, so request.url and request.signal work
contextServer-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.