CodeWithPrashant
Frontend

Fix Stale Search Results in React with AbortController

CodeWithPrashantOct 17, 2026 3 min read 5 views

A search screen can show the wrong results even when every request succeeds. The problem is often response order, not the API itself.

Imagine a learner types “react” and then quickly changes the query to “redis.” The Redis request finishes first. A slower React response arrives afterward and replaces the correct results. This is a race condition: the order in which work finishes differs from the order in which it started.

Decide what the screen should represent

The screen should represent the latest query, including its loading and error state. Older work should not overwrite newer intent.

React’s useEffect documentation explains cleanup between effect runs. AbortController lets you signal cancellation to fetch. Together, these provide a useful foundation for a small client-side example.

import { useEffect, useState } from 'react';

export function SearchResults({ query }) {
  const [state, setState] = useState({
    status: 'idle', items: [], error: ''
  });

  useEffect(() => {
    const text = query.trim();
    if (!text) {
      setState({ status: 'idle', items: [], error: '' });
      return;
    }

    const controller = new AbortController();
    let current = true;
    setState({ status: 'loading', items: [], error: '' });

    async function load() {
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(text)}`,
          { signal: controller.signal }
        );
        if (!response.ok) throw new Error('Search failed');
        const body = await response.json();
        if (!Array.isArray(body.items)) {
          throw new Error('Unexpected search response');
        }
        if (current) {
          setState({ status: 'success', items: body.items, error: '' });
        }
      } catch (error) {
        if (!current || controller.signal.aborted) return;
        setState({
          status: 'error', items: [],
          error: 'Could not load results. Please try again.'
        });
      }
    }

    load();
    return () => {
      current = false;
      controller.abort();
    };
  }, [query]);

  if (state.status === 'idle') return <p>Enter a search term.</p>;
  if (state.status === 'loading') return <p role="status">Searching…</p>;
  if (state.status === 'error') return <p role="alert">{state.error}</p>;
  if (!state.items.length) return <p>No matching resources found.</p>;
  return <ul>{state.items.map(item => <li key={item.id}>{item.title}</li>)}</ul>;
}

This example assumes the API returns an items array with stable IDs and titles. A production app should validate each item as well, using the contract shared with its backend.

Why use cancellation and a current flag?

Cancellation avoids continuing browser-side fetch work where possible. The flag states the ownership rule directly: only the active effect may update this screen. It also protects later steps if your implementation adds asynchronous work that does not accept an abort signal.

Aborting a fetch does not undo a server-side write that already happened. Do not use this pattern as a substitute for idempotent payment, booking, or publishing APIs.

Debouncing solves a different problem

Debouncing reduces the number of requests while the user types. It does not guarantee that older requests cannot finish later. You may need both a debounce and stale-response protection.

For a larger application, use the framework’s data-loading features or a query library for caching, retries, and request coordination rather than repeating the same effect everywhere.

Test the sequence that causes the bug

Delay the first response, let the second complete, and then release the first. Assert that the second query still owns the screen. Also try clearing the search, navigating away, receiving an HTTP error, and returning an empty result.

The UI should distinguish an empty result from a failed request. That distinction helps the learner decide whether to change the query or retry.

Continue with the frontend learning path, and practise explaining why cancellation and debouncing address different concerns.

Comments

Log in to join the discussion.

Loading comments...