Skip to solution
mediumFrontend

How do you handle routing in React applications?

744 views
01

Understand 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.

routingreact routerspanavigation
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — no prior routing knowledge assumed. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. A note on verification: no client router is installed in this repo

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Search params as state, and a hand-rolled router for the smallest case
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 50 of 119 decoded in the React.js track. One more won't hurt.

Back to track