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

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</ >

Test yourself

Q1. Standard router library is…
Q2. For client-side nav, use…
Q3. Read URL params with…

Discussion

Loading…