Topics in this subject
React 4 min read Updated 5 Aug 2026

Routing (React Router)

React Router v6+: routes, nested outlets, hooks, data router loaders/actions, and protected routes.

Setup & basics#

React Router v6 matches the best route (not first-match, not exhaustive), supports relative nested routes, and replaced Switch/component/exact with Routes/element. Every path is exact by default.

import { BrowserRouter, Routes, Route, Link, NavLink } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        {/* NavLink gives you active state for styling */}
        <NavLink to="/users" className={({ isActive }) => isActive ? "on" : ""}>
          Users
        </NavLink>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users/:id" element={<User />} />
        <Route path="*" element={<NotFound />} />   {/* catch-all 404 */}
      </Routes>
    </BrowserRouter>
  );
}

🎯 Link renders an <a> and does client-side navigation (no full reload). NavLink adds isActive/isPending for styling the current route.

Nested routes & <Outlet>#

A parent route renders shared chrome (layout, nav) and <Outlet /> marks where the matched child renders. index is the default child for the parent's exact path.

<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route index element={<Overview />} />         {/* /dashboard */}
    <Route path="stats" element={<Stats />} />      {/* /dashboard/stats */}
    <Route path="users/:id" element={<User />} />   {/* /dashboard/users/42 */}
  </Route>
</Routes>

function DashboardLayout() {
  return (
    <div className="dash">
      <Sidebar />
      <Outlet />   {/* child route renders here */}
    </div>
  );
}
flowchart TD
  A["URL /dashboard/users/42"] --> B["Match route tree"]
  B --> C["DashboardLayout (parent)"]
  C --> D["Sidebar + Outlet"]
  D --> E["User (:id=42) renders in Outlet"]
Hook Returns / does
useNavigate() Imperative navigate: nav("/x"), nav(-1), nav("/x", { replace, state })
useParams() Dynamic segments object, e.g. { id: "42" } (always strings)
useSearchParams() [params, setParams] over the query string (URLSearchParams)
useLocation() Current location (pathname, search, state)
useMatch(pattern) Match info for a pattern, or null
function User() {
  const { id } = useParams();
  const navigate = useNavigate();
  const [params, setParams] = useSearchParams();
  const tab = params.get("tab") ?? "profile";

  return (
    <>
      <h1>User {id} — {tab}</h1>
      <button onClick={() => setParams({ tab: "settings" })}>Settings</button>
      <button onClick={() => navigate("/users", { replace: true })}>Back</button>
    </>
  );
}

⚠️ navigate from useNavigate is stable, but calling it during render throws — do it in effects or handlers. Params are strings; coerce (Number(id)) as needed.

Data router (loaders & actions)#

The data APIs (createBrowserRouter + RouterProvider) let routes fetch data before rendering (loaders) and handle form submissions (actions), enabling pending UI, deferred data, and error boundaries per route.

import { createBrowserRouter, RouterProvider, useLoaderData, redirect } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <User />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not Found", { status: 404 });
      return res.json();                    // available via useLoaderData()
    },
    action: async ({ request, params }) => {
      const form = await request.formData();
      await fetch(`/api/users/${params.id}`, { method: "PUT", body: form });
      return redirect(`/users/${params.id}`);
    },
    errorElement: <RouteError />,
  },
]);

function Root() { return <RouterProvider router={router} />; }
function User() { const user = useLoaderData(); return <h1>{user.name}</h1>; }

🟢 Loaders remove render-then-fetch waterfalls and let you gate navigation. Use <Form> (not raw <form>) to trigger actions without a page reload. React Router v7 (Remix-merged) keeps these APIs.

Protected routes pattern#

Wrap children in a guard that redirects when unauthenticated. Preserve the attempted URL so you can return the user after login.

function RequireAuth({ children }) {
  const user = useAuth();
  const location = useLocation();
  if (!user) return <Navigate to="/login" replace state={{ from: location }} />;
  return children;
}

// usage
<Route path="/admin" element={
  <RequireAuth><AdminPanel /></RequireAuth>
} />

With the data router, put the check in the loader and throw redirect("/login") — it runs before any component mounts, avoiding a flash of protected content. 🎯

Interview Q&A#

Q1. What changed from React Router v5 to v6? SwitchRoutes, componentelement, routes are exact by default, relative nested routes with <Outlet>, and ranked best-match instead of first-match. v6.4 added the data router (loaders/actions).

Q2. Link vs NavLink vs useNavigate? Link is a declarative client-side anchor. NavLink adds isActive/isPending for styling the current route. useNavigate navigates imperatively from handlers/effects.

Q3. What is <Outlet>? A placeholder in a parent route's element where the matched child route renders — the mechanism for nested/layout routes and shared chrome.

Q4. Loaders vs fetching in useEffect? Loaders run during navigation, before the component renders, eliminating render-then-fetch waterfalls and enabling pending states and per-route error boundaries. useEffect fetches only after mount, causing a loading flash.

Q5. How do you implement protected routes? Wrap the element in a guard that returns <Navigate to="/login" replace state={{ from }} /> when unauthenticated, or throw redirect("/login") inside the route's loader for a flash-free redirect.