React Router v7 Best Practices
Quick Reference
Router Setup (Data Mode):
import { createBrowserRouter, RouterProvider } from "react-router";
const router = createBrowserRouter([
{
path: "/",
Component: Root,
ErrorBoundary: RootErrorBoundary,
loader: rootLoader,
children: [
{ index: true, Component: Home },
{ path: "products/:productId", Component: Product, loader: productLoader },
],
},
]);
ReactDOM.createRoot(root).render(<RouterProvider router={router} />);
Framework Mode (Vite plugin):
// routes.ts
import { index, route } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("products/:pid", "./product.tsx"),
];
Route Configuration
Nested Routes with Outlets
createBrowserRouter([
{
path: "/dashboard",
Component: Dashboard,
children: [
{ index: true, Component: DashboardHome },
{ path: "settings", Component: Settings },
],
},
]);
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Outlet /> {/* Renders child routes */}
</div>
);
}
Dynamic Segments and Splats
{ path: "teams/:teamId" } // params.teamId
{ path: ":lang?/categories" } // Optional segment
{ path: "files/*" } // Splat: params["*"]
Key Decision Points
Form vs Fetcher
Use <Form>: Creating/deleting with URL change, adding to history
Use useFetcher: Inline updates, list operations, popovers - no URL change
Loader vs useEffect
Use loader: Data before render, server-side fetch, automatic revalidation
Use useEffect: Client-only data, user-interaction dependent, subscriptions
Gates (decision sequencing)
Answer in order. Pass means the condition is true; pick the API on the same line and stop.
<Form> vs useFetcher
- Must the URL or history stack change (bookmark/share, back returns to prior screen)?
- Pass →
<Form> / route action (or useSubmit + navigation). Stop.
- Fail → Step 2.
- Mutation stays on the same route (inline edit, modal, list row, no address change)?
- Pass →
useFetcher(). Stop.
- Fail → Re-check step 1; you may need a dedicated action route or POST to the current URL.
loader vs useEffect
- Is data needed for correct first render (or your intended
<Suspense> boundary) for this route?
- Pass →
loader (Framework: clientLoader when appropriate). Stop.
- Fail → Step 2.
- Fetch only after mount from user action, timer, or subscription (not route entry)?
- Pass →
useEffect / event handlers. Stop.
- Fail → Prefer loader + revalidation over an effect that mirrors navigation.
Additional Documentation
- Data Loading: See references/loaders.md for loader patterns, parallel loading, search params
- Mutations: See references/actions.md for actions, Form, fetchers, validation
- Navigation: See references/navigation.md for Link, NavLink, programmatic nav
- Advanced: See references/advanced.md for error boundaries, protected routes, lazy loading
Mode Comparison
| Feature |
Framework Mode |
Data Mode |
Declarative Mode |
| Setup |
Vite plugin |
createBrowserRouter |
<BrowserRouter> |
| Type Safety |
Auto-generated types |
Manual |
Manual |
| SSR Support |
Built-in |
Manual |
Limited |
| Use Case |
Full-stack apps |
SPAs with control |
Simple/legacy |
1---2name: react-router-v73description: React Router v7 best practices for data-driven routing. Use when implementing routes, loaders, actions, Form components, fetchers, navigation guards, protected routes, or URL search params. Triggers on createBrowserRouter, RouterProvider, useLoaderData, useActionData, useFetcher, NavLink, Outlet.4---5
6# React Router v7 Best Practices
7
8## Quick Reference
9
10**Router Setup (Data Mode)**:
11```tsx
12import { createBrowserRouter, RouterProvider } from "react-router";
13
14const router = createBrowserRouter([
15 {
16 path: "/",
17 Component: Root,
18 ErrorBoundary: RootErrorBoundary,
19 loader: rootLoader,
20 children: [
21 { index: true, Component: Home },
22 { path: "products/:productId", Component: Product, loader: productLoader },
23 ],
24 },
25]);
26
27ReactDOM.createRoot(root).render(<RouterProvider router={router} />);
28```
29
30**Framework Mode (Vite plugin)**:
31```ts
32// routes.ts
33import { index, route } from "@react-router/dev/routes";
34
35export default [
36 index("./home.tsx"),
37 route("products/:pid", "./product.tsx"),
38];
39```
40
41## Route Configuration
42
43### Nested Routes with Outlets
44
45```tsx
46createBrowserRouter([
47 {
48 path: "/dashboard",
49 Component: Dashboard,
50 children: [
51 { index: true, Component: DashboardHome },
52 { path: "settings", Component: Settings },
53 ],
54 },
55]);
56
57function Dashboard() {
58 return (
59 <div>
60 <h1>Dashboard</h1>
61 <Outlet /> {/* Renders child routes */}
62 </div>
63 );
64}
65```
66
67### Dynamic Segments and Splats
68
69```tsx
70{ path: "teams/:teamId" } // params.teamId
71{ path: ":lang?/categories" } // Optional segment
72{ path: "files/*" } // Splat: params["*"]
73```
74
75## Key Decision Points
76
77### Form vs Fetcher
78
79**Use `<Form>`**: Creating/deleting with URL change, adding to history
80**Use `useFetcher`**: Inline updates, list operations, popovers - no URL change
81
82### Loader vs useEffect
83
84**Use loader**: Data before render, server-side fetch, automatic revalidation
85**Use useEffect**: Client-only data, user-interaction dependent, subscriptions
86
87## Gates (decision sequencing)
88
89Answer **in order**. **Pass** means the condition is true; pick the API on the same line and **stop**.
90
91### `<Form>` vs `useFetcher`
92
931. **Must the URL or history stack change** (bookmark/share, back returns to prior screen)?
94 - **Pass →** `<Form>` / route `action` (or `useSubmit` + navigation). **Stop.**
95 - **Fail →** Step 2.
962. **Mutation stays on the same route** (inline edit, modal, list row, no address change)?
97 - **Pass →** `useFetcher()`. **Stop.**
98 - **Fail →** Re-check step 1; you may need a dedicated action route or POST to the current URL.
99
100### `loader` vs `useEffect`
101
1021. **Is data needed for correct first render** (or your intended `<Suspense>` boundary) for this route?
103 - **Pass →** `loader` (Framework: `clientLoader` when appropriate). **Stop.**
104 - **Fail →** Step 2.
1052. **Fetch only after mount** from user action, timer, or subscription (not route entry)?
106 - **Pass →** `useEffect` / event handlers. **Stop.**
107 - **Fail →** Prefer loader + revalidation over an effect that mirrors navigation.
108
109## Additional Documentation
110
111- **Data Loading**: See [references/loaders.md](references/loaders.md) for loader patterns, parallel loading, search params
112- **Mutations**: See [references/actions.md](references/actions.md) for actions, Form, fetchers, validation
113- **Navigation**: See [references/navigation.md](references/navigation.md) for Link, NavLink, programmatic nav
114- **Advanced**: See [references/advanced.md](references/advanced.md) for error boundaries, protected routes, lazy loading
115
116## Mode Comparison
117
118| Feature | Framework Mode | Data Mode | Declarative Mode |
119|---------|---------------|-----------|------------------|
120| Setup | Vite plugin | `createBrowserRouter` | `<BrowserRouter>` |
121| Type Safety | Auto-generated types | Manual | Manual |
122| SSR Support | Built-in | Manual | Limited |
123| Use Case | Full-stack apps | SPAs with control | Simple/legacy |