# Build a Filter Sidebar with URL State Sync

Source: https://www.techinterview.org/post/3233475336/build-filter-sidebar-with-url-state-sync/
Updated: 2026-07-12 · techinterview.org

"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:

- A query string, `?category=laptops&category=accessories&priceMin=500`, is readable but has a cap on URL length.

- A compact encoded format, base64 of JSON in a single param, is short but opaque and 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 by splitting it 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.
