DeployU
Interviews / Frontend Engineering / This data fetching component has a race condition. How do you fix it?

Questions

This data fetching component has a race condition. How do you fix it?

debugging Data Fetching Interactive Quiz Code Examples
Wrong Approach

I would debounce the input to prevent rapid fetching.

Right Approach

Debouncing helps reduce requests, but doesn't fix the core issue. The real problem is that useEffect doesn't cancel previous in-flight requests. We need to use a cleanup function with AbortController.

The Race Condition Sequence:

  1. userId becomes “2” → fetch starts (2000ms delay)
  2. userId becomes “3” → new fetch starts (500ms delay)
  3. Fetch “3” resolves first → UI shows “Charlie” ✓
  4. Fetch “2” resolves late → UI overwrites to “Bob” ✗

The Fix: Return a cleanup function from useEffect that aborts the previous request when dependencies change.

UserDetails Component - Fix the Race Condition
Your Code
import React, { useState, useEffect } from 'react';

// Mock API with variable delay
const fetchUser = (id) => {
  const users = { '1': { name: 'Alice' }, '2': { name: 'Bob' }, '3': { name: 'Charlie' } };
  const delay = id === '2' ? 2000 : 500;
  return new Promise(resolve => {
    setTimeout(() => resolve(users[id]), delay);
  });
};

export default function UserDetails() {
  const [userId, setUserId] = useState('1');
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    // THE BUG: No cleanup - previous fetch will overwrite state
    fetchUser(userId).then(fetchedUser => {
      setUser(fetchedUser);
      setLoading(false);
    });
  }, [userId]);

  return (
    <div>
      <input type="text" value={userId} onChange={e => setUserId(e.target.value)} />
      <hr />
      {loading ? <p>Loading...</p> : user && <h2>{user.name}</h2>}
    </div>
  );
}
const code = document.querySelector('[data-editor]').textContent;
if (!code.includes('AbortController')) throw new Error('Test failed: Use AbortController to cancel requests.');
if (!code.includes('return ()')) throw new Error('Test failed: Add a cleanup function to abort the request.');
if (!code.includes('.abort()')) throw new Error('Test failed: Call controller.abort() in cleanup.');
console.log('✓ All tests passed! Race condition fixed correctly.');
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Solution

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

  setLoading(true);
  fetchUser(userId, controller.signal)
    .then(fetchedUser => {
      setUser(fetchedUser);
      setLoading(false);
    })
    .catch(error => {
      if (error.name === 'AbortError') return; // Expected
      setLoading(false);
    });

  // Cleanup: abort when userId changes or unmounts
  return () => controller.abort();
}, [userId]);

Practice Question

In a useEffect hook, what is the primary purpose of the returned cleanup function?