Engrain by ByteRush

RCT.04.04 Mid-level· 11 minCore — asked often

Fetching in effects correctly: races, loading states and waterfalls

If you fetch data in useEffect, handle race conditions with an ignore flag or AbortController, model loading and error states, and avoid request waterfalls. And know why frameworks and TanStack Query are usually better.

Comes up in: React, Machine coding

Learn#

Ravi clicks through profiles quickly. Sometimes the page says “user 2” at the top but shows user 1’s details. The two requests raced, and the slower, older one finished last.

A race condition in an effect happens because each effect run starts a request, but nothing stops an older one from finishing last. The cleanup is the fix: it runs before the next effect, so it can mark the old request as outdated:

import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register(); // a DOM for Node; in a browser you already have one
const { createElement: h, useEffect, useState } = await import("react");
const { createRoot } = await import("react-dom/client");

const delays = { 1: 150, 2: 50 }; // user 1's request is the slow one
const fakeFetch = (id) => new Promise((r) => setTimeout(() => r(`profile of user ${id}`), delays[id]));

function Profile({ userId, safe }) {
  const [text, setText] = useState("loading…");
  useEffect(() => {
    let ignore = false;
    fakeFetch(userId).then((data) => {
      if (safe && ignore) return; // a newer request replaced this one
      setText(data);
    });
    return () => { ignore = true; };
  }, [userId, safe]);
  return h("p", null, `${safe ? "with ignore flag" : "no guard"}: user ${userId} → ${text}`);
}
for (const safe of [false, true]) {
  const box = document.createElement("div");
  const root = createRoot(box);
  root.render(h(Profile, { userId: 1, safe }));
  await new Promise((r) => setTimeout(r, 10));
  root.render(h(Profile, { userId: 2, safe })); // the user clicks another profile
  await new Promise((r) => setTimeout(r, 300));
  console.log(box.textContent);
  root.unmount();
}
This example uses Node.js, so it runs in our checks rather than in your browser.

Output (verified)

no guard: user 2 → profile of user 1
with ignore flag: user 2 → profile of user 2

An AbortController aborted in the cleanup also stops the download (JS.11.04).

The slow old request lands lastStep 1 of 4

Network

  • GET /users/1 …

Page

    empty

    userId is 1. The effect starts a request for user 1, which happens to be slow (150 ms).

    Read the steps as text
    1. userId is 1. The effect starts a request for user 1, which happens to be slow (150 ms).
    2. The user clicks user 2. The cleanup for user 1's effect sets ignore = true; a new effect requests user 2 (50 ms).
    3. User 2's response arrives first and is shown.
    4. User 1's response arrives last. Without the flag it overwrites the screen; with it, it is dropped.

    Predict#

    Predict first

    A page fetches the user in a parent effect, then each child fetches its own data in its effect after the user loads. What is the problem?

    Interview angle#

    Say it like this — about 30 seconds

    “Fetching in an effect has three traps. Race conditions: an old request can finish after a new one, so I use an ignore flag or abort the request in the cleanup. Missing states: I model idle, loading, success and error, and keep old data while refetching. And waterfalls: nested components that fetch after their parents load. Raw effects also lack caching and de-duplication, so in real apps I use a framework’s data loading, Server Components, or TanStack Query.”

    Stays on your device. Nothing is uploaded.
    Follow-up How do you prevent race conditions when fetching data in an effect?
    Set an ignore flag (or abort with an AbortController) in the cleanup, and only set state if the request is still current.
    Follow-up What does TanStack Query add over a hand-written useFetch?
    A shared cache, de-duplication, background refetching, retries, cancellation and loading and error states.
    Follow-up How do you avoid waterfalls?
    Start independent requests together, fetch at the route level, prefetch, or use Server Components that load data on the server.

    Practice#

    On your own

    Write the logic of a correct useFetch: loading state, abort, and ignoring outdated responses.

    Done when

    • Each load reports loading, then success or error, only if it is still the latest.
    • The previous request is aborted, and its AbortError is never reported.
    • cancel stops reporting entirely, like an unmount.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    How does an effect avoid showing a stale response?

    An ignore flag or AbortController set in its cleanup.

    Why can't the effect function itself be async?

    It must return a cleanup function or nothing, not a promise.

    What is a request waterfall?

    Requests that start one after another because each waits for the one above.

    Name a library that handles caching and races for you.

    TanStack Query (or SWR).

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round
    Model idle, loading, success and error as one status (TS.02.02). While refetching, keep showing the old data with a small indicator instead of blanking the screen.
    Check yourself: Which four states should a fetch UI model?

    Idle, loading, success and error.

    How it works underneathSenior rounds

    Abort stops the download; the ignore check covers responses that already arrived or APIs that ignore the signal. Report AbortError as nothing, not as a failure Source: React: Fetching data in effects.

    Check yourself: Why use both abort and an ignore check?

    Abort saves bandwidth; the check covers responses that finish before the abort lands or APIs that ignore the signal.

    Edge cases and trapsWhere people slip

    In development, StrictMode runs the effect twice, so you see two requests; with a correct cleanup, the first is ignored or aborted. Production sends one.

    Check yourself: What does StrictMode do to a fetch effect in development?

    Runs it twice; the first request is cleaned up (ignored or aborted).

    In productionStaff-level answers
    Prefer framework loaders, Server Components (RCT.06.03) or TanStack Query (5.105.0) Source: TanStack Query: Overview.
    Load a route’s data in parallel at the top, not in nested effects.
    Check yourself: Where should data for a route ideally be fetched?

    At the route or server level, in parallel, not deep in nested effects.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

    Interview questions this answers

    • How do you prevent race conditions when fetching data in an effect?