We use cookies to enhance your experience on the site
CodeWorlds

Advanced Effects — Race Conditions and Debouncing

In the previous module, you learned the basics of useEffect optimization: properly choosing dependencies, using

isCurrent
flags to prevent race conditions, and simple debouncing. Time for advanced techniques - like a true engineering officer on a spaceship, you must know not only the basic procedures but also emergency protocols for the most complex situations.

AbortController — canceling requests

A simple

isCurrent
flag has a drawback: the request continues running in the background, consuming network resources. When the user quickly switches between pages, dozens of useless requests may be running in the background. It's like sending radio signals to stations you've already flown away from - you're wasting transmitter power.

AbortController
is a built-in browser mechanism that allows you to actually cancel an HTTP request, rather than just ignoring its result.

1function PlanetDetails({ planetId }) {
2  const [planet, setPlanet] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    // Creating controller to cancel request
8    const abortController = new AbortController();
9
10    setLoading(true);
11    setError(null);
12
13    fetch(\`/api/planets/\${planetId}\`, {
14      signal: abortController.signal  // Passing signal to fetch
15    })
16      .then(res => res.json())
17      .then(data => {
18        setPlanet(data);
19        setLoading(false);
20      })
21      .catch(err => {
22        // AbortError means the request was intentionally cancelled
23        if (err.name !== 'AbortError') {
24          setError(err.message);
25          setLoading(false);
26        }
27        // If AbortError - we do nothing because the component no longer exists
28      });
29
30    // Cleanup: cancel request when planetId changes or component unmounts
31    return () => abortController.abort();
32  }, [planetId]);
33
34  if (loading) return <p>Loading planet data...</p>;
35  if (error) return <p>Error: {error}</p>;
36  return <h2>{planet?.name}</h2>;
37}

The key difference: with an

isCurrent
flag, the request continues in the background and consumes bandwidth. With
AbortController
, the browser actually interrupts the network connection. For large files or slow connections, this is a huge saving.

Race conditions — deeper analysis

A race condition in the React context is a situation where the result of an asynchronous operation arrives at the component at the wrong moment. Imagine: an astronaut clicks on planet Mars, then quickly on Jupiter. The request for Mars takes 2 seconds, the one for Jupiter 0.5 seconds. Jupiter returns first - we correctly display Jupiter. But after 2 seconds Mars comes back and overwrites Jupiter's data!

1// PROBLEM: Race condition without protection
2function StarMap({ starId }) {
3  const [star, setStar] = useState(null);
4
5  useEffect(() => {
6    // Every starId change triggers a new fetch
7    // But the previous fetch is still running in the background!
8    fetchStarData(starId).then(data => {
9      setStar(data); // May overwrite newer data!
10    });
11  }, [starId]);
12
13  return <div>{star?.name}</div>;
14}
15
16// SOLUTION: AbortController eliminates the race condition
17function StarMap({ starId }) {
18  const [star, setStar] = useState(null);
19
20  useEffect(() => {
21    const controller = new AbortController();
22
23    fetchStarData(starId, { signal: controller.signal })
24      .then(data => setStar(data))
25      .catch(err => {
26        if (err.name !== 'AbortError') console.error(err);
27      });
28
29    return () => controller.abort(); // Cancel previous request
30  }, [starId]);
31
32  return <div>{star?.name}</div>;
33}

Stale closures — the trap of outdated values

A stale closure is a subtle bug that appears when a callback inside useEffect "sees" an old version of a state variable. It's like reading yesterday's sensor report thinking it's current data.

1// PROBLEM: Stale closure
2function AsteroidCounter() {
3  const [count, setCount] = useState(0);
4
5  useEffect(() => {
6    const timer = setInterval(() => {
7      // This function "sees" count from when the timer was created = always 0!
8      setCount(count + 1); // Always sets to 1 because count = 0
9    }, 1000);
10
11    return () => clearInterval(timer);
12  }, []); // Empty array = effect runs once, count closed over 0
13
14  return <p>Asteroids: {count}</p>;
15}
16
17// SOLUTION 1: Use functional updater
18function AsteroidCounter() {
19  const [count, setCount] = useState(0);
20
21  useEffect(() => {
22    const timer = setInterval(() => {
23      // Functional updater always receives the current value!
24      setCount(prevCount => prevCount + 1);
25    }, 1000);
26
27    return () => clearInterval(timer);
28  }, []);
29
30  return <p>Asteroids: {count}</p>;
31}
32
33// SOLUTION 2: Use useRef for values that must be current
34function TrajectoryCalculator() {
35  const [velocity, setVelocity] = useState(100);
36  const velocityRef = useRef(velocity);
37
38  // Synchronize ref with the latest value
39  useEffect(() => {
40    velocityRef.current = velocity;
41  }, [velocity]);
42
43  useEffect(() => {
44    const timer = setInterval(() => {
45      // ref.current always has the latest value
46      console.log('Current velocity:', velocityRef.current);
47    }, 1000);
48
49    return () => clearInterval(timer);
50  }, []);
51
52  return <input value={velocity} onChange={e => setVelocity(Number(e.target.value))} />;
53}

Rule: If a callback in

setInterval
or
setTimeout
needs the current state value, use a functional updater (
setState(prev => ...)
) or
useRef
.

Advanced cleanup patterns

The cleanup function in useEffect is not just

clearInterval
. In real applications, you need to clean up many types of resources simultaneously.

1function LiveDashboard({ stationId }) {
2  const [data, setData] = useState(null);
3
4  useEffect(() => {
5    const controller = new AbortController();
6    let eventSource = null;
7    let reconnectTimer = null;
8
9    // 1. Fetching initial data
10    fetch(\`/api/station/\${stationId}\`, { signal: controller.signal })
11      .then(res => res.json())
12      .then(setData)
13      .catch(err => {
14        if (err.name !== 'AbortError') console.error(err);
15      });
16
17    // 2. Server-Sent Events for live updates
18    function connectSSE() {
19      eventSource = new EventSource(\`/api/station/\${stationId}/live\`);
20      eventSource.onmessage = (event) => {
21        setData(JSON.parse(event.data));
22      };
23      eventSource.onerror = () => {
24        eventSource.close();
25        // Reconnect after 5 seconds
26        reconnectTimer = setTimeout(connectSSE, 5000);
27      };
28    }
29    connectSSE();
30
31    // 3. Cleanup: close ALL resources
32    return () => {
33      controller.abort();              // Cancel fetch
34      if (eventSource) eventSource.close();  // Close SSE
35      if (reconnectTimer) clearTimeout(reconnectTimer); // Cancel reconnect
36    };
37  }, [stationId]);
38
39  return <div>{data?.status}</div>;
40}

Debounce with cleanup — the correct pattern

In the previous module you learned simple debouncing. Now let's see a pattern that properly handles cleanup and avoids memory leaks:

1function useDebounce(value, delay) {
2  const [debouncedValue, setDebouncedValue] = useState(value);
3
4  useEffect(() => {
5    // Set timer to update debounced value
6    const timer = setTimeout(() => {
7      setDebouncedValue(value);
8    }, delay);
9
10    // Cleanup: cancel timer if value changes before delay expires
11    return () => clearTimeout(timer);
12  }, [value, delay]);
13
14  return debouncedValue;
15}
16
17// Usage in a search component
18function GalaxySearch() {
19  const [query, setQuery] = useState('');
20  const debouncedQuery = useDebounce(query, 400);
21  const [results, setResults] = useState([]);
22
23  // This effect runs only after 400ms from the last character
24  useEffect(() => {
25    if (!debouncedQuery) {
26      setResults([]);
27      return;
28    }
29
30    const controller = new AbortController();
31
32    fetch(\`/api/search?q=\${debouncedQuery}\`, { signal: controller.signal })
33      .then(res => res.json())
34      .then(setResults)
35      .catch(err => {
36        if (err.name !== 'AbortError') console.error(err);
37      });
38
39    return () => controller.abort();
40  }, [debouncedQuery]);
41
42  return (
43    <div>
44      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search the galaxy..." />
45      <ul>
46        {results.map(r => <li key={r.id}>{r.name}</li>)}
47      </ul>
48    </div>
49  );
50}

Notice how we combine three patterns: custom hook (

useDebounce
), AbortController, and cleanup. This is a typical pattern in professional React applications.

Summary

Advanced effect management techniques:

  1. AbortController - cancel requests instead of ignoring responses. Saves bandwidth and resources.
  2. Race conditions - use AbortController or cancel flags to prevent newer data from being overwritten by older data.
  3. Stale closures - use functional updater (
    setState(prev => ...)
    ) or
    useRef
    in timer callbacks.
  4. Complex cleanup - close all resources: fetch, SSE, timers, event listeners.
  5. Debounce + AbortController - combine both patterns for optimal searching.
Go to CodeWorlds