Skip to content

4. Routing (React Router)

React has no built-in router. React Router is the standard choice for client-side routing.

These notes use React Router v6/v7 syntax. If you learned v5 (Switch, useHistory, component={}), the differences are listed at the end — interviewers still ask about them.


1. What Is Client-Side Routing?

In a traditional site, every link triggers a full page request to the server.

In a SPA, the router:

  1. Intercepts the click
  2. Updates the URL with the History API (pushState)
  3. Swaps the component — no page reload, no white flash

Interview Point

The URL changes but the browser never makes a document request. That is why a refresh on /about returns 404 unless the server is configured to serve index.html for all paths — a very common deployment interview question.


2. Basic Setup

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

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Router Types

RouterUsesWhen
BrowserRouterHistory API — clean URLsDefault; needs server rewrite config
HashRouter#/aboutStatic hosts with no rewrite control
MemoryRouterIn-memory, no URLTests, React Native

3. Navigation

jsx
import { Link, NavLink } from "react-router-dom";

<Link to="/about">About</Link>

<NavLink
  to="/about"
  className={({ isActive }) => (isActive ? "active" : "")}
>
  About
</NavLink>

NavLink knows whether it is the current route. Link does not.

Never Use an Anchor Tag

jsx
<a href="/about">About</a>   // full page reload, loses all state
<Link to="/about">About</Link> // correct

Programmatic Navigation

jsx
import { useNavigate } from "react-router-dom";

function Login() {
  const navigate = useNavigate();

  const handleLogin = async () => {
    await api.login();
    navigate("/dashboard", { replace: true });
  };
}

replace: true removes the login page from history, so the back button doesn't return the user to it after logging in. This is the standard answer to "how do you redirect after login?".

jsx
if (!user) return <Navigate to="/login" replace />;

Use this for a redirect during render; use useNavigate inside an event handler or effect.


4. Route Params

jsx
<Route path="/users/:id" element={<UserDetail />} />
jsx
import { useParams } from "react-router-dom";

function UserDetail() {
  const { id } = useParams();  // always a string
  const { data } = useQuery({ queryKey: ["user", id] });
}

Optional and Splat Params

jsx
<Route path="/files/*" element={<FileBrowser />} />   // matches /files/a/b/c
<Route path="/users/:id?" element={<Users />} />      // id optional

5. Query Strings

jsx
import { useSearchParams } from "react-router-dom";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = Number(searchParams.get("page") ?? 1);
  const sort = searchParams.get("sort") ?? "name";

  const nextPage = () => setSearchParams({ page: page + 1, sort });
}

Interview Point

Filters, sorting and pagination belong in the URL, not in useState. It makes the view shareable, bookmarkable, and survivable across a refresh — and the back button works for free.


6. Nested Routes and Outlet

jsx
<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route index element={<Overview />} />
    <Route path="settings" element={<Settings />} />
    <Route path="billing" element={<Billing />} />
  </Route>
</Routes>
jsx
function DashboardLayout() {
  return (
    <div className="layout">
      <Sidebar />
      <main>
        <Outlet />   {/* child route renders here */}
      </main>
    </div>
  );
}

index is the default child, rendered at /dashboard exactly.

The sidebar stays mounted while children swap — no re-render, no flicker.


7. Protected Routes

jsx
function ProtectedRoute({ children }) {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) return <Spinner />;

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}
jsx
<Route
  path="/dashboard"
  element={
    <ProtectedRoute>
      <Dashboard />
    </ProtectedRoute>
  }
/>

Then after login, send them where they were going:

jsx
const from = location.state?.from?.pathname || "/dashboard";
navigate(from, { replace: true });

Layout Route Version

Cleaner when many routes need the same guard:

jsx
<Route element={<ProtectedLayout />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings" element={<Settings />} />
</Route>

Security Note

Route guards are UX only. Anyone can open DevTools and flip the client-side flag. Every protected resource must be authorised on the server as well.


8. Lazy Loading Routes

jsx
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./pages/Dashboard"));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</Suspense>

Route-level code splitting is the highest-value performance win in most React apps — the initial bundle only contains the landing page.


9. Data Router (v6.4+)

The newer API moves data fetching into the route definition, removing the render-then-fetch waterfall.

jsx
const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserDetail />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not Found", { status: 404 });
      return res.json();
    },
    action: async ({ request }) => {
      const formData = await request.formData();
      return updateUser(formData);
    },
    errorElement: <ErrorPage />,
  },
]);

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

Interview Point

Loaders fetch in parallel with the code chunk, before the component renders. The classic useEffect fetch can only start after the component has mounted — a render → fetch → render waterfall.


10. Useful Hooks

HookReturns
useNavigateFunction to navigate programmatically
useParamsRoute params object
useSearchParams[params, setParams] for the query string
useLocation{ pathname, search, hash, state }
useMatchWhether the current URL matches a pattern
useLoaderDataData from the route loader
useNavigationGlobal navigation state (idle / loading / submitting)

11. v5 vs v6 Differences

Still asked, because plenty of production code is on v5.

v5v6 / v7
<Switch><Routes>
component={Home} or render={}element={<Home />}
exact prop neededMatching is exact by default
useHistory()useNavigate()
history.push("/x")navigate("/x")
history.replace("/x")navigate("/x", { replace: true })
Nested routes defined in the childNested <Route> + <Outlet>
withRouter HOCHooks only
Best-match by declaration orderBest-match by specificity (ranked)

12. Re-render on Browser Resize

A frequent "experienced" question that has nothing to do with routing but gets grouped with it.

jsx
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  return width;
}

Follow-up They Want

Resize fires dozens of times per second. Throttle or debounce it:

jsx
useEffect(() => {
  let frame;
  const onResize = () => {
    cancelAnimationFrame(frame);
    frame = requestAnimationFrame(() => setWidth(window.innerWidth));
  };
  window.addEventListener("resize", onResize);
  return () => {
    window.removeEventListener("resize", onResize);
    cancelAnimationFrame(frame);
  };
}, []);

Better still: use a CSS media query if you only need it for styling. JavaScript is the wrong tool for a purely visual breakpoint.

© 2025 DDocs · Dipak's Documentation Guide