Loading...
Loading...
The fourth state layer: shareable, bookmarkable, back-button-correct UI.
Module 8 · URL State & Navigation
The fourth state layer: search params, routing state, shareable and restorable UI, filter/sort/pagination in the URL, and navigation patterns in the App Router.
The fourth state layer: what belongs in the URL versus a store, encoding filters, sort, and pagination in search params, deep linking, scroll restoration, and App Router navigation without history spam.
Most candidates name local, global, and server state and stop there. Senior answers add URL state: the layer that makes a view shareable, bookmarkable, back-button correct, and server-renderable.
This module covers what belongs in the URL versus a store, encoding filters and pagination in search params, deep linking, scroll restoration, optimistic navigation, and how URL state interacts with React Query cache keys.
You will learn the practical mechanics too: useSearchParams and router.replace vs push, avoiding history spam while typing, debouncing URL writes, and keeping SSR and client state in sync without hydration mismatches.
Interviewers use this to separate people who have shipped real dashboards from people who have only built demos: filters that vanish on refresh are the classic tell.
Phase 3 (Week 3) of the interview roadmap. Senior Track spans modules 1–19; the Staff Track starts at module 20.
Interviewers use URL state to separate people who have shipped real product surfaces from people who have built demos. Filters that disappear on refresh, a broken back button, or a dashboard link that cannot be shared with a teammate are all symptoms of state parked in the wrong layer.
Most candidates name three. The fourth is where senior answers start.
Frontend state is usually described as local, global, and server state. That model is incomplete. The URL is a state store too: it survives reload, it is readable on the server before React runs, and it is the only layer a user can copy and send to a colleague.
| Layer | Lives in | Survives reload | Shareable |
|---|---|---|---|
| Local | Component memory | No | No |
| Global | Store (Zustand/Redux) | Only if persisted | No |
| Server | Query cache + backend | Refetched | No |
| URL | Address bar + history | Yes | Yes |
Say it explicitly: “I put anything a user would share or bookmark in the URL, server data in a query cache, cross-cutting UI preferences in a store, and everything else in local state.” That one sentence covers most of a state-management round.
The test: would a user be annoyed if this reset on refresh?
| Put in URL | Keep out of URL |
|---|---|
| Search query | Unsaved form input |
| Filters and sort | Dropdown open/closed |
| Page or cursor | Hover and focus state |
| Active tab or step | Toast visibility |
| Selected entity id | Scroll velocity |
| Date range | Anything containing a secret |
Tokens, session identifiers, and personal data. URLs leak into browser history, server access logs, analytics tools, and the Referer header sent to third-party origins.
Keep params readable, short, and stable enough to be a cache key.
// Parse once, at the boundary, into a typed object. type FeedParams = { q: string; sort: "new" | "top"; page: number }; function parseFeedParams(sp: URLSearchParams): FeedParams { const sort = sp.get("sort"); const page = Number(sp.get("page") ?? "1"); return { q: sp.get("q")?.slice(0, 100) ?? "", sort: sort === "top" ? "top" : "new", page: Number.isFinite(page) && page > 0 ? Math.min(page, 500) : 1, }; }
Two URLs that render the same view should be the same string. Otherwise you fragment your CDN cache, your analytics, and your query cache keys across meaningless variants.
The classic dashboard requirement, and the classic interview follow-up.
Filters drive the request, so they should drive the URL. Once the URL is the source of truth, the server can render the first page with data already resolved, and the back button becomes free.
function setFilter(key: string, value: string | null) { const next = new URLSearchParams(searchParams); if (value === null || value === "") next.delete(key); else next.set(key, value); next.delete("page"); // any filter change invalidates the cursor router.replace(`${pathname}?${next}`, { scroll: false }); }
Keep the input responsive while the URL lags slightly behind.
const [text, setText] = useState(() => searchParams.get("q") ?? ""); useEffect(() => { const id = setTimeout(() => { const next = new URLSearchParams(searchParams); text ? next.set("q", text) : next.delete("q"); next.delete("page"); router.replace(`${pathname}?${next}`, { scroll: false }); }, 300); return () => clearTimeout(id); }, [text]);
A modal that cannot be linked to is a modal you will be asked to fix.
If a modal shows meaningful content, it deserves an address. Route it, so that sharing the link opens it directly, back closes it, and the server can render its contents for SEO and first paint.
Mentioning that back should close the modal shows you have shipped this. It is the single most common bug in home-grown modal routing.
URL state is the only client state the server can read.
// Server Component: data is resolved before HTML is sent. export default async function FeedPage({ searchParams, }: { searchParams: Promise<Record<string, string | string[]>>; }) { const params = parseFeedParams(new URLSearchParams(await searchParams)); const posts = await getPosts(params); return <Feed initialPosts={posts} params={params} />; }
Wire the URL into the data layer and navigation becomes cached.
When the parsed params are part of the cache key, going back does not refetch, it reads from cache. Forward navigation to a previously visited filter set is instant, and stale responses cannot overwrite the current view because they belong to a different key.
const params = parseFeedParams(useSearchParams()); const { data } = useQuery({ queryKey: ["feed", params], // URL change ⇒ key change ⇒ correct data queryFn: () => fetchFeed(params), placeholderData: keepPreviousData, // avoid layout flash while refetching });
A late response for ?q=sho lands under its own key and is simply ignored by the view rendering ?q=shoes. Key-based caching is the most robust of the standard race-condition fixes.
Returning from a detail page should not dump the user at the top.
What to say when asked about state management.
URL state and navigation interview questions.