Routes & Links
React Routers v6/v7 route tree is the single source of truth for the URL surface: paths, layouts, loaders, actions, error boundaries. Author it as data (createBrowserRouter), declare nested children for layouts, and use the small helpers (Outlet, NavLink, useRouteError) to wire it up.
A real route tree with nested layouts and lazy chunks
EXAMPLE
// npm i react-router-dom
import {
createBrowserRouter, RouterProvider,
Outlet, NavLink, useRouteError, isRouteErrorResponse,
defer, Await, redirect,
} from 'react-router-dom';
import { Suspense, lazy } from 'react';
// 1) Lazy-load page modules so each route ships its own chunk
const Catalog = lazy(() => import('./pages/Catalog'));
const Cart = lazy(() => import('./pages/Cart'));
// 2) Route definitions live in one file
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
// index route — renders at '/' inside Layout
{ index: true, loader: homeLoader, element: <Home /> },
// Public, lazy-loaded section
{
path: 'catalog',
element: (
<Suspense fallback={<p>Loading catalog...</p>}>
<Catalog />
</Suspense>
),
loader: () => fetch('/api/catalog').then((r) => r.json()),
},
// Auth-gated section with its own layout
{
path: 'account',
element: <AccountLayout />,
loader: requireAuth, // redirects to /login if not signed in
id: 'account', // useRouteLoaderData('account')
children: [
{ index: true, element: <AccountHome /> },
{ path: 'orders', element: <Orders />, loader: ordersLoader },
{
path: 'orders/:id',
element: <OrderDetail />,
loader: orderLoader,
action: orderAction,
errorElement: <ResourceMissing />,
},
],
},
{ path: 'login', element: <Login />, action: loginAction },
{ path: 'logout', loader: logoutLoader },
// 404 fallback (must be LAST among siblings)
{ path: '*', element: <NotFound /> },
],
},
]);
// 3) Top-level layout with header / nav / outlet
function Layout() {
return (
<div>
<header>
<NavLink to='/' end>Home</NavLink>
<NavLink to='/catalog'>Catalog</NavLink>
<NavLink to='/account/orders'>Orders</NavLink>
</header>
<main><Outlet /></main>
</div>
);
}
// 4) Section layout — extra layout depth for /account routes
function AccountLayout() {
return (
<div className='grid grid-cols-[200px_1fr]'>
<aside>
<NavLink to='/account' end>Overview</NavLink>
<NavLink to='/account/orders'>Orders</NavLink>
</aside>
<section><Outlet /></section>
</div>
);
}
// 5) Loaders + actions live alongside the route definitions
async function homeLoader() {
// defer() lets the page render while slow data streams in via <Await>
return defer({
featured: fetch('/api/featured').then((r) => r.json()),
});
}
async function ordersLoader() {
const res = await fetch('/api/orders');
if (!res.ok) throw new Response('failed', { status: res.status });
return res.json();
}
async function orderLoader({ params }) {
const res = await fetch('/api/orders/' + params.id);
if (res.status === 404) throw new Response('not found', { status: 404 });
return res.json();
}
async function orderAction({ request, params }) {
const body = await request.formData();
if (body.get('intent') === 'cancel') {
await fetch('/api/orders/' + params.id + '/cancel', { method: 'POST' });
return redirect('/account/orders');
}
return null;
}
async function requireAuth() {
const res = await fetch('/api/me');
if (!res.ok) throw redirect('/login?next=' + encodeURIComponent(location.pathname));
return null;
}
async function loginAction({ request }) {
const body = await request.formData();
const res = await fetch('/api/login', { method: 'POST', body });
if (!res.ok) return { error: 'wrong credentials' };
const next = new URL(request.url).searchParams.get('next') ?? '/';
return redirect(next);
}
async function logoutLoader() {
await fetch('/api/logout', { method: 'POST' });
return redirect('/');
}
// 6) Centralised error UI — runs for every route under errorElement
function ErrorPage() {
const err = useRouteError();
if (isRouteErrorResponse(err)) {
return <h1>HTTP {err.status}: {err.statusText}</h1>;
}
return <h1>Something broke: {(err as Error)?.message ?? 'unknown'}</h1>;
}
// 7) Render the whole thing at the root
export default function App() {
return <RouterProvider router={router} />;
}
// Stubs
function Home() { return <p>Welcome</p>; }
function AccountHome() { return <p>Overview</p>; }
function Orders() { return <p>Orders</p>; }
function OrderDetail() { return <p>Detail</p>; }
function Login() { return <form method='post'><input name='email'/><button>Sign in</button></form>; }
function ResourceMissing() { return <p>Order not found.</p>; }
function NotFound() { return <p>404</p>; }
Why it matters
Author the route tree as data, not JSX scattered through the app. The single source of truth makes nested layouts, lazy chunks, and loader/action boundaries visible at one glance — and refactoring a section becomes "edit one branch of the tree" instead of "find every nested Switch".
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<User />} />
</Routes>
Try it Yourself »
Discussion
Loading…