Today I Understood useEffect Cleanup & Race Conditions (Real Lessons from usePopcorn)
Source: Dev.to

Fast Typing Broke My App (But Slowly Revealed the Truth)
Early in the app, I noticed a strange behavior:
- Slow searches → correct results
- Fast typing → errors or incorrect data
At first it looked like an API issue, but it was actually a race condition. Multiple requests were being fired, and older responses sometimes updated state after newer ones.
Race Conditions: The Right Mental Model
A race condition happens when React receives responses out of order. The fix isn’t to “wait” or “delay” requests; it’s to cancel outdated work. This is where AbortController comes in:
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort(); // cleanup
The cleanup ensures that only the latest request is allowed to finish, stabilizing the app.
Cleanup Functions Are Not Optional
Before, I treated cleanup functions like a bonus. Now I understand that cleanup is part of the effect’s lifecycle.
A simple example with the document title:
document.title = `Movie | ${title}`;
return () => {
document.title = "usePopcorn";
};
Without cleanup, side effects leak into future renders. With cleanup, React stays in control.
Cleanup Prevents Silent Bugs (Event Listeners)
Event listeners also need proper cleanup:
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
Without cleanup:
- Listeners stack up
- One key press triggers multiple handlers
With cleanup:
- Old listeners are removed
- Behavior stays predictable
A Bug I Fixed Earlier — But Truly Understood Today
The race condition issue was something I had already fixed with external help. Today, however, I didn’t just apply a fix—I understood the system behind it. That shift from fixing to understanding is what real learning feels like.
How I Think About useEffect Now
- Effects run
- Cleanup runs before the next effect
- Cleanup also runs on unmount
- Async effects must always be cancelable
Once this clicked, useEffect stopped feeling unpredictable.
Final Thoughts
Today reminded me of something important: clean code isn’t about writing more; it’s about writing only what’s necessary. Cleanup functions and race‑condition handling are not advanced tricks—they are foundational React skills.
I’m still learning, still improving, and still refining my understanding. Today’s clarity made the journey feel worth it.