Skip to the solution
MediumFrontend7 min read

How do you handle routing in React applications?

744 views

The problem

Question presented to candidate: "React has no built-in router. How do you choose one, and what does that choice actually commit you to?"

What a strong answer should cover:

  • React has no router. It renders a tree; mapping URLs to trees is a separate concern, which is why several credible options exist.
  • The real fork is client-side routing versus server-side routing, because it decides where data loading lives β€” not which API you prefer.
  • Client routing (React Router, TanStack Router): the URL is client state, the router swaps components, navigation is instant after load. Data loading is yours to arrange.
  • Server routing (Next.js App Router and similar): the URL maps to files, the server can render and stream the new segment, and data loading is part of the route.
  • The critical shared idea is route-level data loading β€” starting the fetch on navigation rather than after the component mounts, which is what avoids the classic waterfall.
  • Every router must handle the same list: nested layouts, URL params and search params, code splitting per route, pending and error states, and scroll restoration.
  • Search params are state β€” filters, sort order, pagination β€” and putting them in the URL makes them shareable, bookmarkable and back-button-correct for free.
  • A tiny app may need nothing: conditional rendering on a state value is a legitimate answer for two screens.

Clarifying questions expected:

  • "Is this content public and crawlable, or an authenticated app?" β€” that leans the client/server decision.
  • "Do we already have a framework?" β€” the router usually comes with it.

Code / implementation expected: Optional. Naming the decision axis matters more than API syntax.

  • routing
  • react router
  • spa
  • navigation

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactSearch params as state, and a hand-rolled router for the smallest case
import { useState, useEffect, useSyncExternalStore } from "react";

// Two things worth demonstrating without any router dependency:
// 1. the smallest possible "router" β€” for an app that genuinely needs none
// 2. why filters belong in the URL rather than in useState

// ── A minimal hash router, ~15 lines ───────────────────────────────────────
const hashStore = {
  subscribe(cb) {
    window.addEventListener("hashchange", cb);
    return () => window.removeEventListener("hashchange", cb);
  },
  get: () => window.location.hash.slice(1) || "/",
  getServer: () => "/",
};
const useHash = () =>
  // The third argument is the SERVER snapshot β€” without it, server rendering
  // throws "Missing getServerSnapshot".
  useSyncExternalStore(hashStore.subscribe, hashStore.get, hashStore.getServer);

const parse = (hash) => {
  const [path, query = ""] = hash.split("?");
  return { path, params: new URLSearchParams(query) };
};

const ITEMS = [
  { id: 1, name: "Keyboard", tag: "input" },
  { id: 2, name: "Mouse", tag: "input" },
  { id: 3, name: "Monitor", tag: "display" },
  { id: 4, name: "Laptop", tag: "display" },
];

export default function App() {
  const hash = useHash();
  const { path, params } = parse(hash);

  // βœ… The filter lives in the URL: shareable, bookmarkable, back-button-correct.
  const urlTag = params.get("tag") || "all";
  // ❌ The same filter in component state, for contrast.
  const [stateTag, setStateTag] = useState("all");

  const go = (p, q) => { window.location.hash = p + (q ? "?" + q : ""); };

  const shown = ITEMS.filter((i) => urlTag === "all" || i.tag === urlTag);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <nav style={{ display: "flex", gap: 8, marginBottom: 10 }}>
        {["/", "/about"].map((p) => (
          <button key={p} onClick={() => go(p)} style={{ fontWeight: path === p ? "bold" : "normal" }}>
            {p}
          </button>
        ))}
      </nav>

      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12 }}>
        {path === "/about" ? (
          <p style={{ fontSize: 13, margin: 0 }}>
            An about page. Fifteen lines of router, no dependency β€” genuinely
            enough for two or three screens with no deep linking.
          </p>
        ) : (
          <>
            <div style={{ fontSize: 13, marginBottom: 6 }}>
              <strong>βœ… filter in the URL:</strong>{" "}
              {["all", "input", "display"].map((t) => (
                <button key={t} onClick={() => go("/", t === "all" ? "" : "tag=" + t)}
                        style={{ fontWeight: urlTag === t ? "bold" : "normal", marginRight: 4 }}>
                  {t}
                </button>
              ))}
            </div>
            <div style={{ fontSize: 13, marginBottom: 8 }}>
              <strong>❌ the same filter in useState:</strong>{" "}
              {["all", "input", "display"].map((t) => (
                <button key={t} onClick={() => setStateTag(t)}
                        style={{ fontWeight: stateTag === t ? "bold" : "normal", marginRight: 4 }}>
                  {t}
                </button>
              ))}
            </div>
            <ul style={{ fontSize: 13, margin: 0 }}>
              {shown.map((i) => <li key={i.id}>{i.name} <em style={{ color: "#888" }}>({i.tag})</em></li>)}
            </ul>
          </>
        )}
      </div>

      <p style={{ fontSize: 12, color: "#666", marginTop: 8, fontFamily: "ui-monospace, monospace" }}>
        location.hash = {JSON.stringify(hash)}
      </p>

      <p style={{ fontSize: 13, color: "#666" }}>
        Change the URL filter, then press the browser back button β€” it undoes
        the filter, and the address bar always describes what is on screen. Do
        the same with the state filter: back leaves the page entirely, and a
        copied link shows a different view than the one you were looking at.
        That is why search params are state.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 50 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you handle memory leaks in React?

Continue