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.