React Router
Route configuration, dynamic routes, nested routes, navigation, loaders
React Router (v6+) is the standard routing library for React applications. It provides client-side routing with nested routes, data loading (loaders), form handling (actions), and error boundaries — moving React closer to a full framework for data-driven apps.
Key Points
- createBrowserRouter: define routes as a data structure — enables loaders, actions, and error boundaries per route
- Outlet: renders the matched child route component — enables nested layouts (header/sidebar stay, content changes)
- <Link>: declarative navigation; useNavigate(): programmatic navigation — both update history without full page reload
- Route params: /users/:id → useParams() → { id: "123" }
- Search params: /search?q=react → useSearchParams() — persistent, shareable URL state
- Loaders (v6.4+): async function on route that fetches data before component renders — useLoaderData() retrieves it
- Actions (v6.4+): handle form submissions — POST to the same route triggers action, then revalidates loaders
- errorElement: route-level error boundary — catches loader/action errors and render errors
- Lazy: { lazy: () => import('./Component') } — code-split route components automatically
React Router v6: data router, loaders, actions, Outlet for layouts, lazy routes, useSearchParams
import { createBrowserRouter, RouterProvider, Outlet, useLoaderData,
Link, useNavigate, useParams, useSearchParams, redirect } from 'react-router-dom';
// Data router — loaders + actions
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />, // Outlet renders children
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'users',
loader: usersLoader, // called before component renders
element: <UserList />
},
{
path: 'users/:id',
loader: userLoader,
action: updateUserAction, // handles <Form method="post">
element: <UserDetail />,
errorElement: <UserNotFound />
},
{
path: 'settings',
lazy: () => import('./pages/Settings').then(m => ({ Component: m.default }))
}
]
}
]);
// Loader — runs before component, parallel with siblings
export async function userLoader({ params }: LoaderFunctionArgs) {
const user = await fetchUser(params.id!);
if (!user) throw new Response('Not Found', { status: 404 });
return user;
}
// Action — handle form POST
export async function updateUserAction({ request, params }: ActionFunctionArgs) {
const formData = await request.formData();
await updateUser(params.id!, Object.fromEntries(formData));
return redirect(`/users/${params.id}`);
}
// Component — gets loader data synchronously
function UserDetail() {
const user = useLoaderData() as User;
const navigate = useNavigate();
return (
<div>
<h1>{user.name}</h1>
<Form method="post">
<input name="name" defaultValue={user.name} />
<button type="submit">Save</button>
</Form>
<button onClick={() => navigate(-1)}>Back</button>
</div>
);
}
// Search params — shareable filter state
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';
return (
<select value={category} onChange={e => setSearchParams({ category: e.target.value })}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
);
}Real-World Example
React Router's loaders run in parallel — navigating to /users/123 fires the userLoader and parent layoutLoader simultaneously, not sequentially. Compared to the old pattern (useEffect in component → fetch on mount → render → show loading spinner), loaders mean data is ready before the component renders — no loading flicker and no waterfall requests.