React Router Cheatsheet
Navigation and Links
Use this React Router reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Link
<Link> is a real anchor that the router intercepts. In Framework mode
it also drives lazy route discovery and prefetching.
| Prop | Values | Notes |
|---|---|---|
to | string or { pathname, search, hash } | Relative unless it starts with / |
prefetch | none (default), intent, render, viewport | Framework mode |
replace | boolean | Swap the history entry instead of pushing |
state | any serializable value | Read with useLocation().state |
relative | route (default), path | path uses URL segments, not nesting |
reloadDocument | boolean | Plain browser navigation, no client routing |
preventScrollReset | boolean | Keep the scroll position |
viewTransition | boolean | Wraps the update in a View Transition |
discover | render (default), none | Lazy route discovery for this link |
import { Link } from "react-router"; <Link to="/about">About</Link> <Link to="../sibling" relative="path">Sibling</Link> <Link to="/search?q=router" prefetch="intent">Search</Link> <Link to="/login" state={{ from: "/checkout" }}>Sign in</Link>
prefetch is implemented with a <link rel="prefetch"> element rendered
next to the anchor, so a CSS selector like nav a:last-child can stop
matching once you add it.
Redirects from Loaders and Actions
| Helper | Effect |
|---|---|
redirect(url, init?) | 302 by default, client side navigation |
redirect(url, 301) | Pass a number for the status |
redirectDocument(url) | Full document load, use for a route outside the app |
replace(url) | Redirect that replaces the history entry |
import { redirect } from "react-router"; import type { Route } from "./+types/dashboard"; export async function loader({ request }: Route.LoaderArgs) { const user = await getUser(request); if (!user) throw redirect("/login"); return { user }; }
Throwing is the idiom inside a helper function, since it stops execution at the call site. Returning the redirect works too when it is the last statement in the loader.
Pending UI
import { useNavigation, useRevalidator } from "react-router"; function GlobalSpinner() { const navigation = useNavigation(); const busy = navigation.state !== "idle"; return busy ? <div className="bar" /> : null; }
| Read | Value |
|---|---|
navigation.state | idle, loading, or submitting |
navigation.location | Where the app is heading, undefined when idle |
navigation.formData | The submitted FormData, for optimistic UI |
navigation.formMethod | get, post, put, patch, delete |
useRevalidator().revalidate() | Re-run every loader on the page |
useRevalidator().state | idle or loading |
Framework mode also lets a route export HydrateFallback for the initial
client load, so pending UI splits into two cases: first paint and later
navigations.
Link or Form
| Situation | Use |
|---|---|
| Read only navigation | <Link> |
| Anything that mutates | <Form method="post"> |
| Mutation with no URL change | useFetcher and <fetcher.Form> |
| A filter that lives in the URL | <Form method="get"> or setSearchParams |
| Nav that needs a computed target | useNavigate in a handler |
A <Form method="get"> is a navigation, so it updates the URL and reruns
loaders. A <Form method="post"> calls the route action, then
revalidates everything on the page.