React Router
React Router is the standard client-side routing library. The modern v6 / v7 API is declarative: routes are described as a tree, nested routes share layouts via Outlet, data loaders run before render, and actions handle form submissions. The same router can power SPAs, RSC apps, and the framework-mode Remix experience.
Routes, loaders, actions, nested layouts, errors
EXAMPLE
// npm i react-router-dom
import {
createBrowserRouter, RouterProvider,
Outlet, NavLink, useLoaderData, useParams,
useNavigate, useLocation, useNavigation,
redirect, isRouteErrorResponse, useRouteError,
} from 'react-router-dom';
// 1) Route definitions — a tree with loaders + actions
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'orders',
loader: ordersLoader,
element: <Orders />,
children: [
{
path: ':id',
loader: orderLoader,
action: orderAction,
element: <OrderDetail />,
},
],
},
{
path: 'login',
action: loginAction,
element: <Login />,
},
{
path: 'protected',
loader: requireAuth, // redirects in the loader if not signed in
element: <Protected />,
},
],
},
]);
// 2) Provide once at the root
export default function App() {
return <RouterProvider router={router} />;
}
// 3) Layout with nav + Outlet for nested routes
function Layout() {
const nav = useNavigation(); // 'idle' | 'loading' | 'submitting'
return (
<div>
<nav className='flex gap-3 p-3 border-b'>
<NavLink to='/' end>Home</NavLink>
<NavLink to='/orders' >Orders</NavLink>
<NavLink to='/login' >Login</NavLink>
</nav>
{nav.state === 'loading' && <progress className='w-full' />}
<main className='p-4'><Outlet /></main>
</div>
);
}
// 4) Loaders fetch data BEFORE the route renders — no spinners in components
async function ordersLoader() {
const res = await fetch('/api/orders');
if (!res.ok) throw new Response('failed', { status: res.status });
return res.json();
}
function Orders() {
const orders = useLoaderData() as any[];
return (
<ul>{orders.map((o) => (
<li key={o.id}>
<NavLink to={\`/orders/${o.id}\`}>{o.customer} — ${o.total}</NavLink>
</li>
))}</ul>
);
}
// 5) Param loader + action
async function orderLoader({ params }: { params: any }) {
const res = await fetch(\`/api/orders/${params.id}\`);
if (!res.ok) throw new Response('order missing', { status: 404 });
return res.json();
}
async function orderAction({ request, params }: any) {
const data = await request.formData();
const intent = data.get('intent');
if (intent === 'cancel') {
await fetch(\`/api/orders/${params.id}/cancel\`, { method: 'POST' });
return redirect('/orders');
}
return null;
}
// 6) Auth gate as a loader (no flash of forbidden content)
async function requireAuth() {
const ok = await fetch('/api/me');
if (!ok.ok) throw redirect('/login?next=/protected');
return null;
}
// 7) Login form -> action
async function loginAction({ request }: any) {
const body = await request.formData();
const res = await fetch('/api/login', { method: 'POST', body });
if (!res.ok) return { error: 'wrong email or password' };
const params = new URL(request.url).searchParams;
return redirect(params.get('next') ?? '/');
}
// 8) Programmatic navigation when you need it (rare with actions/loaders)
function Home() {
const navigate = useNavigate();
return <button onClick={() => navigate('/orders')}>See orders</button>;
}
function OrderDetail() {
const o = useLoaderData() as any;
return (
<div>
<h2>Order {o.id}</h2>
<form method='post'>
<button name='intent' value='cancel'>Cancel</button>
</form>
</div>
);
}
// 9) Centralised error UI
function ErrorPage() {
const err = useRouteError();
if (isRouteErrorResponse(err)) {
return <p>HTTP {err.status}: {err.statusText}</p>;
}
return <p>Something went wrong: {(err as Error).message}</p>;
}
function Login() { return <form method='post'><input name='email' /><input name='password' type='password' /><button>Sign in</button></form>; }
function Protected() { return <p>Welcome.</p>; }
Why it matters
Loaders + actions move data-fetching and mutations into the router, so route transitions are race-free, errors land at one place, and you stop scattering useEffect+useState through every screen. Add loaders incrementally — even one or two on the busiest routes removes most of the loading-state plumbing you would have written by hand.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// npm install react-router-dom
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([{ path: '/', element: <Home /> }]);
Try it Yourself »
Exercise
Client-side navigation component.
<
to="/users">Users</
>
Four letters; PascalCase.
Discussion
Loading…