iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…