AlgoInsight

Q&A

How to easily ignore useEffect HTTP calls with ReactJS

By AlgoInsight Team2026-06-07networkingreact-hooks

Overview

useEffect is where data fetching commonly lives, but effects can run multiple times (dependency changes, Strict Mode double-invoke) and components can unmount mid-request. If an earlier request resolves after a newer one (or after unmount), you can render stale data or update a gone component. The remedy is the effect's cleanup function, which lets you ignore or abort the obsolete call.

graph TD
    A[useEffect runs: start fetch] --> B[Deps change / unmount]
    B --> C[Cleanup runs FIRST]
    C --> D[Set ignore=true OR abort the request]
    D --> E[Stale response is discarded]
    A --> F[New effect run starts a fresh fetch]
    style D fill:#1e1b4b,color:#fff

The Problem

useEffect(() => {
  fetch(`/api/items/${id}`)
    .then(res => res.json())
    .then(data => setItems(data));   // ⚠ might set stale data or update after unmount
}, [id]);

If id changes quickly, two requests race; whichever resolves last "wins," which may be the older one. And if the component unmounts before the response, you update a non-existent component.

Solution 1: The ignore flag (simplest)

The cleanup function sets a flag that the resolved promise checks before updating state:

useEffect(() => {
  let ignore = false;                       // closure flag for THIS effect run

  fetch(`/api/items/${id}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) setItems(data);          // only apply if still current
    });

  return () => { ignore = true; };          // cleanup: mark this run stale
}, [id]);

On each re-run or unmount, React calls the previous run's cleanup first, flipping its ignore to true, so its late response is discarded. This is the pattern React's own docs recommend.

Solution 2: AbortController (actually cancels the request)

AbortController not only ignores but cancels the in-flight HTTP request, saving bandwidth:

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

  fetch(`/api/items/${id}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => setItems(data))
    .catch(err => {
      if (err.name !== "AbortError") throw err;   // ignore intentional aborts
    });

  return () => controller.abort();          // cleanup: cancel the request
}, [id]);

Use this when you want to stop wasted network work, not just ignore the result.

With async/await

You can't make the effect callback itself async (it would return a promise, not a cleanup), so define an inner function:

useEffect(() => {
  let ignore = false;
  (async () => {
    const res = await fetch(`/api/items/${id}`);
    const data = await res.json();
    if (!ignore) setItems(data);
  })();
  return () => { ignore = true; };
}, [id]);

React Strict Mode

In development, React 18 Strict Mode intentionally runs effects twice to surface missing cleanup. Proper cleanup (ignore flag or abort) makes your effect resilient to this double-invoke - which is exactly the behavior you want in production for rapid dependency changes too.

The Bigger-Picture Answer: Data-Fetching Libraries

For real apps, libraries like React Query (TanStack Query) or SWR handle all of this - request deduplication, cancellation, caching, stale handling, retries - so you rarely hand-roll fetch-in-useEffect. Mentioning them shows you know the production-grade solution.

Why This Matters in Interviews

React interviews use this to test understanding of effect lifecycle and race conditions. Key points: effects re-run and components unmount, so late responses can be stale; the cleanup function is the fix; the ignore flag pattern discards stale results, while AbortController cancels the request; you can't make the effect callback async directly; and Strict Mode double-invokes effects to expose missing cleanup. Recommending React Query/SWR for production is a maturity signal.

Common Pitfalls

References

More answers