Skip to the solution
MediumMachine Coding13 min read

Build a Tabs Component with URL Sync & Lazy Mounting

814 views

The problem

Question presented to candidate: "We have a dashboard with Overview, Activity and Settings tabs. Product wants the selected tab in the URL, as ?tab=settings, so a link opens the right tab, a reload keeps it, and the browser Back button goes to the previous tab. Activity loads data, so its panel should not be created until someone opens it, but once opened it should keep its state. We do not use a router on this page; use the platform APIs."

Requirements:

  • Tabs come from a config array (id, label, panel component). The accessible tabs pattern (roles, aria-selected, roving tabindex) is assumed; arrow keys, Home and End move focus, and Enter or Space selects (manual activation).
  • The URL is the source of truth: the selected tab is read from ?tab= on load and validated; an unknown value falls back to the default and is corrected with history.replaceState (no extra history entry).
  • Selecting a tab calls history.pushState, keeping every other query parameter and the hash; selecting the tab that is already selected does nothing.
  • Back and Forward (popstate) switch tabs without pushing new entries.
  • Panels are mounted lazily on first visit and stay mounted afterwards, so a loaded list or typed text survives switching away and back.

What a strong answer should cover:

  • Why the URL, not component state, is the source of truth, and the read, validate, write, listen loop with URLSearchParams, pushState, replaceState and popstate.
  • When to push versus replace, and why arrow-key focus movement must not create history entries (hence manual activation).
  • Lazy mounting with a "mounted so far" set plus hiding, versus eager rendering or unmounting, and the effect on data fetching and panel state.
  • Cleaning up the popstate listener, and fetch effects that survive React StrictMode's double invocation.
  • How the same model maps to a router (useSearchParams, Vue Router query, Angular queryParamMap) when one exists.

Clarifying questions expected:

  • "Should every tab change create a history entry, or should switching tabs replace the current entry?"
  • "What should an invalid or missing ?tab= do: fall back silently, or rewrite the URL?"
  • "Should a panel that was visited and left be kept alive, or re-created on the next visit?"

Code / implementation expected: Yes. A working tabs component with URL sync and lazy panels, plus a way to demonstrate Back, Forward and a reload.

  • React
  • Tabs
  • URL State
  • Accessibility

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

13 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useEffect, useState } from "react";
import UrlTabs from "./src/components/UrlTabs";
import { ActivityPanel, OverviewPanel, SettingsPanel } from "./src/utils/panels";

const TABS = [
  { id: "overview", label: "Overview", Panel: OverviewPanel },
  { id: "activity", label: "Activity", Panel: ActivityPanel },
  { id: "settings", label: "Settings", Panel: SettingsPanel },
];

const currentQuery = () => window.location.search || "(empty)";

export default function App() {
  const [query, setQuery] = useState(currentQuery);
  // bump to remount the tabs, like a page reload
  const [generation, setGeneration] = useState(0);

  useEffect(() => {
    const onPop = () => setQuery(currentQuery());
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  return (
    <main className="page">
      <h1>Project dashboard</h1>
      <div className="toolbar">
        <button type="button" onClick={() => window.history.back()}>
          Back
        </button>
        <button type="button" onClick={() => window.history.forward()}>
          Forward
        </button>
        <button type="button" onClick={() => setGeneration((g) => g + 1)}>
          Remount tabs
        </button>
      </div>
      <p className="url">
        URL query: <code data-testid="query">{query}</code>
      </p>
      <UrlTabs
        key={generation}
        tabs={TABS}
        defaultId="overview"
        label="Project sections"
        onNavigate={() => setQuery(currentQuery())}
      />
    </main>
  );
}

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 39 of 107 in Machine coding

Keep your momentum going.

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

Up nextMedium

Build a Tag / Chip Input

Continue