How would you manage the filtered results state and the interaction with an API
Decide where filtering lives: server-side for large data (filters become query params, debounced) or client-side for small data (memoized derive from a source list — don't store filtered results as separate state). Sync filters to the URL, handle loading/empty/error per filter change, and cancel stale requests.
Two questions here: where does filtering happen, and how do you model the state without bugs.
Decision 1: server-side vs client-side filtering
- Large/growing dataset → filter on the server. The filter values are query params; changing a filter triggers a (debounced) request; the response is the filtered result.
- Small, fully-loaded dataset → filter on the client, derived from the source list in memory.
This decision drives everything else.
Decision 2: don't store filtered results as separate state
The most common bug: keeping filteredResults in its own useState alongside the source list and the filters. Now you have three pieces of state that can desync — you update the filter but forget to recompute, or update the list but not the filtered copy.
Filtered results are derived state — compute them, don't store them.
Client-side:
const [items, setItems] = useState([]); // source of truth
const [filters, setFilters] = useState({}); // source of truth
const filtered = useMemo( // DERIVED — not state
() => applyFilters(items, filters),
[items, filters]
);Server-side — the filters are the state; the "results" live in your data layer's cache:
const [filters, setFilters] = useState({});
const debouncedFilters = useDebouncedValue(filters, 300);
const { data, status } = useQuery(["items", debouncedFilters],
() => fetchItems(debouncedFilters)); // results = query result, not separate stateThe supporting concerns
- Debounce filter inputs (especially text) so you don't fire a request per keystroke.
- Cancel stale requests — an
AbortController(or React Query) so an old filter's slow response can't overwrite the current one. - Loading / empty / error states — each filter change is an async cycle; show a subtle refetch indicator (keep old results visible), and a proper "no matches" empty state.
- Sync filters to the URL (
?category=x&q=y) — makes filtered views shareable, bookmarkable, and refresh-safe; the URL becomes the source of truth for filters. - Reset pagination to page 1 whenever filters change.
- keepPreviousData — don't blank the list to a skeleton on every filter tweak.
The framing
"First I decide where filtering lives — server-side for large data, where filters are debounced query params, or client-side for small data. Then the key modeling rule: filtered results are derived, not their own state — useMemo from the source list on the client, or the query result on the server. Storing a separate filteredResults is the classic desync bug. Around that I debounce inputs, cancel stale requests, handle loading/empty/error per change, sync filters to the URL so views are shareable, and reset pagination on filter change."
Follow-up questions
- •Why shouldn't you store filtered results in their own useState?
- •When does filtering belong on the server vs the client?
- •Why sync filters to the URL?
- •How do you prevent a stale filter's response from overwriting the current one?
Common mistakes
- •Storing filteredResults as separate state that desyncs from the source and filters.
- •Firing an API request on every keystroke with no debounce.
- •Not cancelling stale requests — out-of-order responses.
- •Not syncing filters to the URL — filtered views aren't shareable or refresh-safe.
- •Forgetting to reset to page 1 when filters change.
Performance considerations
- •Client-side: memoize the derived filtered list so it's not recomputed on unrelated renders. Server-side: debounce to cut requests, cache per filter combination, and use keepPreviousData to avoid render thrash.
Edge cases
- •Rapid filter changes causing out-of-order responses.
- •A filter combination that returns zero results.
- •Dataset growing until client-side filtering no longer scales.
- •Filters in the URL on initial load (deep link).
Real-world examples
- •A product listing with category/price/search filters reflected in the URL.
- •React Query keyed on the filter object, debounced, with cancellation.