Build a Filter Sidebar with URL State Sync

“Build a filter sidebar” is a frontend machine-coding question that comes up in e-commerce (Amazon), SaaS dashboards (Linear, Jira), and admin tools. The interview probes URL state sync, debouncing, multi-selection patterns, and the experience of a shared link reproducing exactly the sender’s view.

Clarify scope

  • What filter types? (single-select, multi-select, range slider, text search, date range)
  • How many filter sections? (10–30 in real apps)
  • Server-side filtering or client-side? (both is common)
  • Share-by-URL required? (yes for most products)
  • Mobile layout? (slide-over sheet vs persistent sidebar)

The state model

One canonical state object. Every filter type maps to a primitive or array:

{
  query: "macbook",
  category: ["laptops", "accessories"],
  brand: ["apple"],
  priceMin: 500,
  priceMax: 2000,
  inStock: true,
  sort: "price_asc",
  page: 2
}

This object is the source of truth. URL is its serialized form. Filter UI reads from it; mutations produce a new state.

URL serialization

Two common formats:

  • Query string: ?category=laptops&category=accessories&priceMin=500 — readable, cap on URL length
  • Compact encoded: base64 of JSON, single param — short, opaque, harder to debug

Query string is the right default. Use a library like qs for arrays.

Sync direction

  • State change → push to URL (replace, not push, to avoid back-button noise on every keystroke)
  • URL change → parse to state (on initial load, on browser back/forward)

Trap: do not create a loop. Mutate URL only when state changes meaningfully; mutate state only when URL changes from external source (browser nav).

Debouncing

  • Free-text search: debounce 200–300 ms before updating URL and refetching
  • Range slider: debounce on drag-end, not on every move
  • Checkbox toggles: immediate
  • Number inputs: debounce 400 ms

Why debounce: each URL update can trigger a server fetch and a history entry. Save both.

Multi-select pattern

  • Each option as a checkbox
  • “Show more” reveals additional options
  • Counts per option (“Apple (43)”) if backend provides facets
  • “Clear all” button per section
  • Aggregate “Clear filters” at the top

Range slider

  • Two thumbs, draggable
  • Manual numeric inputs as alternative
  • Histogram of distribution behind the slider (Amazon-style)
  • Snap to reasonable steps

Active filter chips

Show selected filters as removable chips at the top:

  • “Category: Laptops × Accessories”
  • “Price: $500 – $2000”
  • X button removes that specific filter

This is the highest-leverage feedback element. Users get lost without it.

Empty / loading states

  • Loading: skeleton results, do not blank the list
  • Empty: “No results match your filters. Clear all
  • Stale: subtle indicator while a debounced fetch is pending

Mobile considerations

  • Slide-over sheet or full-screen modal
  • Sticky “Apply” and “Clear” footer
  • Apply on close, not on every change (saves taps and renders)
  • Keyboard does not cover the active input

Accessibility

  • Each section is a <fieldset> with <legend>
  • Checkboxes have proper labels
  • Range slider follows the WAI-ARIA slider pattern
  • Filter chips: each is a button with “Remove filter X” label
  • Live region announces result count after a change

Performance

  • Server-side filtering for the result list; client-side for facet counts where small
  • Use React.memo on filter section components
  • Avoid re-rendering the whole sidebar on every change — split into independent sections

Edge cases interviewers love

  • User shares URL with a filter the recipient doesn’t have access to (e.g., a deleted category) — graceful fallback
  • User pastes a URL with too many filters — handle URL length
  • Browser back/forward should restore exact state
  • User clears all filters then hits back — restore the previous filters
  • Filter combinations that produce zero results — show actionable empty state

What separates senior from staff

Senior: state model + URL sync + debounce. Staff: handles the share-by-link case carefully (versioning the URL schema), discusses faceted search server expectations, and handles the bidirectional state-URL sync without infinite loops.

Frequently Asked Questions

Library options for production?

nuqs (React) is excellent for URL-state sync. tanstack-router has built-in support. For older stacks, react-router’s useSearchParams plus your own debouncing.

How do I version the URL schema?

Add a v param. On parse, check version; if older, migrate. Simple. Keep migrations in code; do not break old URLs.

What about indexing for SEO?

Filtered URLs should be canonicalized to a base URL with rel=canonical unless they are SEO-strategic landing pages. Otherwise duplicate-content concerns arise.

Scroll to Top