Volume 9

Q379: Async UI Drill Search Filters Cancellation and Stale Responses

Difficulty: SeniorFrequency: HighAnswer time: 20-25 minutes

What Interviewers Want To Evaluate

This drill tests whether you can build asynchronous UI that behaves correctly under real user speed and real network conditions.

Search and filtering questions are common because they expose many frontend fundamentals at once.

The interviewer wants to see state ownership, cancellation, debouncing, stale response prevention, URL state, accessibility, loading states, and test strategy.

Short Interview Answer

For async search UI, I model query and filters separately from results, debounce noisy input when appropriate, cancel or sequence requests, and ensure only the latest request can update visible results. I also handle loading, empty, error, and partial states, keep shareable filters in the URL when needed, and test slow network and out-of-order responses.

Core Problem

Users can:

type quickly
change filters repeatedly
navigate back
refresh the page
lose connection
submit the same search twice
clear input
receive slow responses

The UI must not assume a perfect network.

It must be designed for overlap.

State Model

Separate:

draft query
committed query
filters
sort
pagination
request status
results
error
selected item

Draft query is what the user is typing.

Committed query is what the app is searching.

That distinction makes debouncing and submit-on-enter flows easier to reason about.

URL Ownership

Put state in the URL when it should survive:

refresh
sharing
back and forward navigation
deep links
saved views

Keep state local when it is temporary:

input focus
open dropdown
hovered row
unsaved draft
transient loading spinner

Search pages usually benefit from URL query params.

Small pickers inside forms may not.

Stale Response Bug

Problem:

request A searches "rea"
request B searches "react"
request B finishes first
request A finishes later
UI shows "rea" results while input says "react"

This is a classic async UI bug.

Senior engineers should name it quickly.

Prevention Pattern

Use cancellation:

useEffect(() => {
  const controller = new AbortController();

  async function runSearch() {
    setState({ status: "loading" });

    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: controller.signal,
      });
      const results = await response.json();
      setState({ status: "success", results });
    } catch (error) {
      if (controller.signal.aborted) return;
      setState({ status: "error", message: "Search failed." });
    }
  }

  runSearch();

  return () => controller.abort();
}, [query]);

Aborting the old request prevents it from updating state.

Sequence Pattern

Sometimes cancellation is not enough or the API cannot be aborted.

Use a sequence guard:

const requestIdRef = useRef(0);

useEffect(() => {
  const requestId = requestIdRef.current + 1;
  requestIdRef.current = requestId;

  async function runSearch() {
    const results = await search(query);

    if (requestId !== requestIdRef.current) {
      return;
    }

    setResults(results);
  }

  runSearch();
}, [query]);

Only the latest request is allowed to commit.

Debounce Trade-Off

Debounce helps when:

typing fires many requests
API cost matters
results are not needed on every keystroke
network noise creates flicker

Avoid too much debounce when:

search must feel instant
local filtering is cheap
the user explicitly presses enter
accessibility feedback becomes delayed

Debounce is a product experience decision, not only a performance trick.

Loading UX

Possible loading states:

initial skeleton
inline spinner
dim old results while refreshing
optimistic filter chips
progress text for long operations

For search, keeping old results visible while showing refresh state can feel better than blanking the page.

But the UI must make it clear that results are updating.

Accessibility

Search UI should support:

labelled input
keyboard filter controls
focus order
screen reader result count
announced loading changes when meaningful
clear button with accessible label
semantic list or table
no focus loss after results update

Fast async updates should not make keyboard users lose their place.

Error and Empty States

Separate:

no query yet
no matching results
network failure
server error
unauthorized
invalid filter

These are not the same user message.

A good empty state helps the user adjust search terms.

A good error state helps them retry or recover.

Testing Drill

Test:

debounced request fires after delay
old request cannot overwrite new results
abort happens on query change
empty state appears
error state appears
URL params initialize search
back button restores previous filters
keyboard can use filters

The stale response test is the highest-signal test.

It proves production thinking.

Interview Answer Structure

Use:

requirements
state ownership
request lifecycle
stale response prevention
URL and navigation
loading empty error states
accessibility
testing
trade-offs

This turns a simple search prompt into a senior answer.

Common Mistakes

  • Firing a request on every keystroke without considering cost.
  • Not preventing stale responses.
  • Clearing results during every refresh and causing flicker.
  • Hiding URL state that should be shareable.
  • Forgetting empty and error states.
  • Losing focus after results update.
  • Testing only the happy path.

Final Mental Model

Async search UI is:

state ownership
request lifecycle
cancellation
latest result wins
clear recovery
accessible feedback

The hard part is not fetching data.

The hard part is keeping the UI truthful while the world changes underneath it.