
The category page as a single coherent view: scrollable category pills, a dual-handle price slider, star-rating filters, and sort tabs — all wired to one query state that reports how many products match at every step.
The question: can filters feel like one machine instead of a pile of checkboxes?
Every control writes into a single filter object; every read derives the visible list. There is exactly one source of truth, so "clear filters" is one line — setFilters({}) — and the result count is always correct because it is always computed, never cached.
const [filters, setFilters] = useState<Filters>({});
const results = useMemo(
() => products.filter(matches(filters)),
[products, filters],
);
What went right: URL sync. ?category=&price_min=&price_max= means a filtered view is shareable and survives a refresh for free.
What went wrong: the price slider. Dual-handle drag with ₫ formatting, debounced writes, and no feedback loop into the input boxes took three rewrites. Sliders are deceptively hard.