React useEffect Common Mistakes
Avoid the most common useEffect mistakes — missing dependencies, infinite loops, stale closures, and effects that should not be effects in the first place.
What you'll learn
- ✓Why most things should not be effects
- ✓How to read the dependency array correctly
- ✓Avoiding infinite loops
- ✓Handling cleanup and race conditions
- ✓When to use refs instead of state in effects
Prerequisites
- •Familiarity with JavaScript basics
The useEffect hook is one of the most powerful tools in React, but it is also one of the easiest to misuse. Effects are an escape hatch into the world outside React, and using them incorrectly leads to bugs that are hard to track down. This tutorial walks through the most common mistakes.
Using Effects for Derived State
The first mistake is using useEffect to compute a value that depends on props or state. Effects run after render, so you cause an unnecessary extra render and a flash of stale UI.
// Wrong
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Right
const fullName = `${firstName} ${lastName}`;
If derivation is expensive, wrap it in useMemo. Either way, do not push it into state via an effect.
Missing Dependencies
The dependency array tells React when to rerun the effect. Leaving out a value that the effect uses leads to stale closures, where the effect sees old props or state forever.
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, []); // Bug: count is captured once
Enable the react-hooks/exhaustive-deps ESLint rule. It catches missing dependencies automatically and saves countless hours of debugging.
Infinite Loops
When the dependency array includes a value that the effect modifies, you get an infinite loop. The classic version is setting state inside an effect that depends on that state.
useEffect(() => {
setUser({ ...user, lastSeen: Date.now() });
}, [user]); // user changes, effect reruns, user changes...
Either remove the dependency, use a functional updater, or move the logic out of an effect entirely.
Object and Array Dependencies
Objects and arrays compared by reference can also cause loops. Inline literals are a new reference every render.
// Bad
useEffect(() => {
fetchData(options);
}, [{ id: 1 }]); // New object each render!
Move stable values outside the component, or memoize them with useMemo. For primitive ids, prefer passing the id rather than the whole object.
Forgetting Cleanup
Many effects need cleanup — subscriptions, timers, event listeners, abort controllers. Return a cleanup function from the effect.
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
Without cleanup, you leak listeners and memory. In React 18 development mode, effects run twice on mount, which surfaces missing cleanup right away.
Race Conditions in Async Effects
When an effect fires off a request, a later render can fire off another. If the second response arrives first, you can show stale data.
useEffect(() => {
let cancelled = false;
async function load() {
const data = await fetch(`/users/${id}`).then(r => r.json());
if (!cancelled) setUser(data);
}
load();
return () => { cancelled = true; };
}, [id]);
The cancelled flag prevents the late response from updating state. An AbortController is even better when the network layer supports it.
Effects That Should Be Event Handlers
If something should happen in response to a user action, do it in the event handler — not in an effect that listens to state changes. Effects exist for synchronization with external systems, not for reacting to clicks.
// Wrong: showing a toast when the count crosses a threshold via effect
useEffect(() => {
if (count === 10) toast('Reached 10');
}, [count]);
// Right
function handleClick() {
const next = count + 1;
setCount(next);
if (next === 10) toast('Reached 10');
}
The handler version runs exactly when the user clicked, not at some indirect later time.
Reading the Latest State with Refs
Sometimes inside an interval or async callback you want the latest state without recreating the effect. Refs let you read mutable values without affecting render.
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
useEffect(() => {
const id = setInterval(() => console.log(countRef.current), 1000);
return () => clearInterval(id);
}, []);
This pattern keeps the timer alive across rerenders while still reading fresh values.
Wrapping Up
Most useEffect bugs trace back to a few categories — using effects when you do not need them, lying about dependencies, or forgetting cleanup. Start by asking whether an effect is really required, then make the deps honest, and your React code will be far calmer.
Related articles
- JavaScript React Custom Hooks Patterns
Learn powerful patterns for building React custom hooks — extracting logic, composing hooks, handling cleanup, and designing clean APIs your team will love.
- JavaScript React Hooks Deep Dive: useState to useRef and When to Use Each
A practical deep dive into useState, useEffect, useMemo, useCallback, and useRef. Learn when to reach for each hook and the traps that ruin React code.
- JavaScript React Context API Guide
Learn how to use React's Context API to share state across your component tree without prop drilling — with patterns, performance tips, and common mistakes to avoid.
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.