Back to React
React
medium
mid

How would you manage filtered result state and its 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.

5 min read·~8 min to think through

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:

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

jsx
const [filters, setFilters] = useState({});
const debouncedFilters = useDebouncedValue(filters, 300);
const { data, status } = useQuery(["items", debouncedFilters],
  () => fetchItems(debouncedFilters));        // results = query result, not separate state

The 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.

Senior engineer discussion

Seniors lead with the server-vs-client decision, insist filtered results are derived not stored, sync filters to the URL, debounce and cancel requests, and handle the full async state cycle per filter change.

Related questions