Volume 3

Q177: React Data Fetching, Effects and Race Conditions

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

This question checks whether you understand the hazards of fetching in effects. Interviewers want stale responses, cancellation, cleanup, loading and error state, dependency arrays, waterfalls, Suspense alternatives, server fetching, and why data fetching is more than useEffect.

Senior answers should explain request ownership and race prevention.

Short Interview Answer

Fetching in useEffect can work for client-only data, but it needs cleanup, stale response protection, loading state, error state, and dependency correctness. Race conditions happen when an older request finishes after a newer request and overwrites the UI. Use AbortController, request IDs, ignore flags, or a server-state library. In Next.js, prefer Server Components for server-readable data and use client fetching only where browser ownership is required.

Detailed Interview Answer

Basic client fetch:

useEffect(() => {
  let ignore = false;

  async function load() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();

    if (!ignore) {
      setUser(data);
    }
  }

  load();

  return () => {
    ignore = true;
  };
}, [userId]);

The cleanup prevents a stale response from updating state after the effect is no longer current.

Race Condition Example

User selects account A.

Request A starts.

User quickly selects account B.

Request B starts.

Request B finishes first and shows account B.

Request A finishes later and incorrectly overwrites the UI with account A.

That is a stale response race.

AbortController

Abort old work where possible.

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

  async function load() {
    try {
      const response = await fetch(url, { signal: controller.signal });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name !== "AbortError") {
        setError(error);
      }
    }
  }

  load();

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

Abort protects network and state lifecycle when supported.

Loading State

Loading state should match the UX.

initial loading
background refetch
stale while refreshing
mutation pending
pagination loading
empty result
partial failure

One isLoading boolean is often not enough for rich flows.

Error State

Errors need classification.

network error
timeout
unauthorized
forbidden
not found
validation error
server error
parse error
aborted request

Different errors deserve different recovery paths.

Dependency Arrays

The effect should list values it reads from render scope.

If a fetch depends on userId, locale, or token, those dependencies matter.

Hiding dependencies can produce stale requests.

If dependencies change too often, stabilize inputs or move logic.

Do not silence dependency warnings without understanding ownership.

Waterfalls

Fetching in nested effects can create waterfalls.

render parent
parent effect fetches
render child
child effect fetches
render grandchild
grandchild effect fetches

Server fetching, route loaders, preloading, or parallel query APIs can avoid this.

Server Components Alternative

If data is available to the server and needed for initial render, Server Components are often better.

export default async function Page() {
  const [user, invoices] = await Promise.all([getUser(), getInvoices()]);
  return <Dashboard user={user} invoices={invoices} />;
}

This avoids client fetch waterfalls and can improve first meaningful content.

When Client Fetching Is Appropriate

Client fetching is useful for:

browser-only APIs
live polling after hydration
user-triggered client-only filters
websocket subscriptions
client cache updates
background refresh
data requiring client-only credentials

It should be intentional.

Common Mistakes

A common mistake is updating state after unmount.

Another mistake is letting stale responses win.

Another mistake is putting async functions directly as the effect callback.

Another mistake is missing dependencies.

Another mistake is fetching everything client-side in a Server Component capable app.

Senior Trade-Offs

Manual effect fetching is fine for simple cases, but complex caching, retries, deduplication, optimistic updates, and refetch rules are usually better handled by framework data APIs or server-state libraries.

The senior decision is choosing where the request should be owned.

Debugging Workflow

When client data behaves incorrectly:

log request start and finish ids
simulate slow network
change inputs rapidly
verify cleanup
check abort behavior
inspect dependency array
look for waterfalls
classify loading and error states
compare with server fetching option

Interview Framing

Define the race condition, show cleanup or abort, discuss loading/error states, dependency arrays, waterfalls, and when server fetching is better.

Revision Notes

Fetching in effects is a lifecycle problem. The hard part is ownership and stale work, not the fetch call.

Key Takeaways

Every async request needs a current owner, a stale response strategy, and a recovery path.

Follow-Up Questions

  1. Why can effect fetching race?
  2. How does AbortController help?
  3. What is a stale response?
  4. Why should effect dependencies be complete?
  5. What is a request waterfall?
  6. When is Server Component fetching better?
  7. When is client fetching necessary?
  8. How should loading state be modeled?
  9. How should errors be classified?
  10. When should you use a server-state library?

How I Would Answer This In A Real Interview

I would say effect fetching must handle stale responses, cleanup, loading and error state, and dependency correctness. I would prevent races with aborts or request ownership, avoid waterfalls with server or parallel fetching, and use a server-state library when caching and invalidation become complex.