React Router v7 is a powerful data-first, file-based routing framework for React apps. Instead of treating routing as a side concern, it lets you define UI, data fetching, and navigation logic together — just like real-world apps need.
Here’s your developer-friendly cheatsheet, filled with code examples and explanations for where you'd use each one.
1. Define Routes via routes.ts
// routes.ts
import { route, layout, index } from "@react-router/dev/routes";
export default [
layout("layouts/main.tsx", [
index("routes/home.tsx"),
route("posts/:postId", "routes/post.tsx"),
route("posts/:postId/edit", "routes/edit-post.tsx"),
]),
route("about", "routes/about.tsx"),
];
When to use:
Define your entire app’s routing structure in one place.
layout(...)
wraps child routes in shared UI, route(...)
maps a path to a component, and index(...)
handles default views like dashboards.
2. Layouts + Nested Views via <Outlet />
// layouts/main.tsx
import { Outlet, Link } from "react-router";
export default function MainLayout() {
return (
<>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<main>
<Outlet />
</main>
</>
);
}
When to use:
To wrap multiple routes under a common layout (like sidebar, header, or auth guard). <Outlet />
is where the child route’s content renders.
3. Data Loading with loader()
// routes/post.tsx
import { getPostById } from "../data";
import type { Route } from "./+types/post";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPostById(params.postId);
if (!post) throw new Response("Not Found", { status: 404 });
return { post };
}
export default function Post({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
return <h1>{post.title}</h1>;
}
When to use:
For fetching data before rendering the route component. Ideal for detail pages, dashboards, and static content with dynamic params.
4. Mutate Data with action()
// routes/edit-post.tsx
import { updatePost } from "../data";
import { redirect } from "react-router";
export async function action({ params, request }: Route.ActionArgs) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updatePost(params.postId, updates);
return redirect(`/posts/${params.postId}`);
}
When to use:
For handling form submissions. Use with <form method="post">
to trigger this action on submit and redirect the user afterward.
5. Navigation with <Link>
, <NavLink>
, and useNavigate
<Link to="/posts/123">View Post</Link>
<NavLink to="/about" className={({ isActive }) => isActive ? "active" : ""}>
About
</NavLink>
const navigate = useNavigate();
<button onClick={() => navigate(-1)}>Back</button>
When to use:
For any kind of page navigation — links, menus, back buttons, breadcrumbs, or redirects after actions.
6. Forms Without Navigation using useFetcher
import { useFetcher } from "react-router";
function Favorite({ postId, isFavorite }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post">
<button name="favorite" value={(!isFavorite).toString()}>
{isFavorite ? "★" : "☆"}
</button>
</fetcher.Form>
);
}
When to use:
If you want to mutate data (like a "like" button) without changing the URL or navigating away. Works just like a form, but in-place.
7. Optimistic UI with fetcher.formData
const isNowFavorite = fetcher.formData
? fetcher.formData.get("favorite") === "true"
: contact.favorite;
When to use:
To instantly update the UI with the user's action before waiting for the server. If the update fails, React Router will revert it automatically.
8. Show Loading State with useNavigation
import { useNavigation } from "react-router";
const navigation = useNavigation();
const isLoading = navigation.state === "loading";
When to use:
To show a spinner, dim content, or animate transitions while new data or routes are loading. Especially useful for slow network or SSR.
9. Search with GET
Form + useSubmit
<Form method="get" role="search" onChange={(e) => submit(e.currentTarget)}>
<input name="q" placeholder="Search..." defaultValue={q || ""} />
</Form>
When to use:
For search inputs, filters, or pagination where you want the query to update the URL (e.g., /search?q=term
). Also supports browser history & refresh.
10. Error Handling via throw new Response(...)
export async function loader({ params }) {
const data = await getData(params.id);
if (!data) throw new Response("Not Found", { status: 404 });
return { data };
}
When to use:
To gracefully handle 404s or validation errors. Throw a Response
from loader/action, and React Router will show the error boundary.
Bonus: History Stack Control with replace
submit(formRef, { replace: true });
When to use:
To avoid creating a new browser history entry when submitting a search or form (e.g., instant search updates that shouldn't pollute back button stack).
Enable SSR or Prerendering (Optional)
// react-router.config.ts
export default {
ssr: true, // enable server-side rendering
prerender: ["/about"], // prerender this static route
};
When to use:
For SEO, faster time-to-interactive, or static deployments. Works with loader()
to fetch and render HTML at build or runtime.
Summary
React Router v7 gives you a unified model for:
-
Navigation with
<Link>
,<NavLink>
,useNavigate
-
Data Fetching with
loader()
-
Mutations with
action()
and<Form>
-
Global and local UI state with
useNavigation
,useFetcher
-
Optimistic UI with
fetcher.formData
- Rendering strategies: SPA, SSR, Prerender
Pro Tip: You don’t need useState
, useEffect
, or even useReducer
for most CRUD apps. React Router v7 gives you a framework where URL = state, form = API call, and route = data boundary.
Want to Learn More?
If you found this article useful and want to go even deeper into React Router v7, check out my new book:
More Effective React Router: Learn React Router v7 in Depth
The book covers 125+ practical chapters about:
- Declarative and Data Routing
- Loaders and Actions
- Advanced Navigation Patterns
- Testing, SSR, Accessibility, and more
Kindle Edition on Amazon
Paperback on Amazon
Built for intermediate to advanced React developers.
Top comments (0)