React Router Cheatsheet
Actions and Forms
Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Actions
An action handles a non-GET submission. After it resolves, the router automatically re-runs the loaders for the active routes, so your UI updates without any cache invalidation code.
{
path: "users/:id",
element: <User />,
loader: userLoader,
action: async ({ request, params }) => {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await deleteUser(params.id);
return redirect("/users");
}
const name = String(formData.get("name") ?? "");
if (!name) return data({ errors: { name: "Required" } }, { status: 400 });
await updateUser(params.id, { name });
return { ok: true };
},
}| Reading the body | Use |
|---|---|
| A normal form | await request.formData() |
| A JSON fetcher submit | await request.json() |
| Raw | await request.text() |
| Method | request.method |
Form
<Form> is a progressively-enhanced <form>. It posts to the route's action over fetch instead of reloading the page, and it works without JavaScript in framework mode.
import { Form } from "react-router"; <Form method="post"> <input name="name" /> <button type="submit">Save</button> </Form> <Form method="post" action="/users/42">…</Form> <Form method="delete">…</Form> <Form method="get" action="/search">…</Form> {/* updates search params */} <Form method="post" encType="multipart/form-data">…</Form> <Form method="post" replace>…</Form> <Form method="post" navigate={false}>…</Form> {/* like a fetcher */} <Form method="post" preventScrollReset>…</Form> <Form reloadDocument method="post">…</Form> {/* real browser submit */}
A <Form method="get"> is the cleanest search box you can write: the inputs become search params, the loader reads them, and the result is a shareable URL with no state management at all.
<Form method="get" action="/search"> <input type="search" name="q" defaultValue={q} /> <select name="sort" defaultValue={sort}> <option value="new">Newest</option> <option value="top">Top</option> </select> <button>Search</button> </Form>
Action Data and Validation
import { useActionData, useLoaderData } from "react-router"; function EditUser() { const { user } = useLoaderData(); const actionData = useActionData(); // undefined until the action returns return ( <Form method="post"> <input name="name" defaultValue={user.name} aria-invalid={!!actionData?.errors?.name} /> {actionData?.errors?.name && ( <p role="alert">✗ {actionData.errors.name}</p> )} <button>Save</button> </Form> ); }
Returning validation errors from the action, rather than throwing, keeps the user's typed values on screen and puts the error next to the field.
Pending UI
import { useNavigation } from "react-router"; function GlobalSpinner() { const navigation = useNavigation(); // navigation.state: "idle" | "loading" | "submitting" const busy = navigation.state !== "idle"; return busy ? <div role="status">Loading…</div> : null; }
| Field | Is |
|---|---|
state | idle, loading, or submitting |
location | Where we are navigating to |
formData | The submitted data, during a submission |
formAction | The action URL |
formMethod | The method |
json / text | The body for non-form submissions |
// A submit button that disables and relabels itself function SubmitButton({ children }) { const navigation = useNavigation(); const submitting = navigation.state === "submitting"; return ( <button type="submit" disabled={submitting}> {submitting ? "Saving…" : children} </button> ); }
Because navigation.formData is available while the request is in flight, you can render the submitted values immediately for an optimistic update, then let revalidation replace them with the server's version.
Fetchers
A fetcher talks to a loader or action without navigating. Use it for anything that is not a page transition: a like button, an inline delete, a combobox, a poll.
import { useFetcher } from "react-router"; function LikeButton({ postId, liked }) { const fetcher = useFetcher(); // Optimistic: trust the in-flight submission over the loaded data const optimistic = fetcher.formData ? fetcher.formData.get("liked") === "true" : liked; return ( <fetcher.Form method="post" action={`/posts/${postId}/like`}> <input type="hidden" name="liked" value={String(!optimistic)} /> <button aria-pressed={optimistic}>{optimistic ? "♥" : "♡"}</button> </fetcher.Form> ); }
const fetcher = useFetcher(); fetcher.load("/api/search?q=react"); // call a loader fetcher.submit(formData, { method: "post", action: "/items" }); fetcher.submit({ q: "react" }, { method: "get", action: "/search" }); fetcher.submit(data, { method: "post", encType: "application/json" }); fetcher.state // "idle" | "loading" | "submitting" fetcher.data // whatever the loader or action returned fetcher.formData // in-flight submission data fetcher.Form // a <Form> that does not navigate
Each useFetcher() call is independent, so a list of rows each with its own delete button gets per-row pending state for free. Give a fetcher a key when you need to share its state across components: useFetcher({ key: "cart" }).
Revalidation Control
import { useRevalidator } from "react-router"; const revalidator = useRevalidator(); revalidator.revalidate(); // re-run every active loader revalidator.state; // "idle" | "loading"
// Skip re-running an expensive loader when it cannot have changed { path: "settings", loader: settingsLoader, shouldRevalidate: ({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }) => { if (formMethod === "post") return true; if (currentUrl.pathname === nextUrl.pathname) return false; return defaultShouldRevalidate; }, }
Loaders re-run after every action by default. That default is correct far more often than it is wasteful, so reach for shouldRevalidate only when you have measured a real problem.