React useEffect: Common Mistakes and How to Avoid Them
By Pawan Singh · Jun 2026
Mistake 1: Missing Dependencies
Omitting a value the effect actually uses from the dependency array causes stale closures — the effect keeps using an old value even after it changes:
// Bug: effect always uses the userId from the first render
useEffect(() => {
fetchUser(userId);
}, []); // userId is used inside, but missing from the array
// Fixed
useEffect(() => {
fetchUser(userId);
}, [userId]);
The eslint-plugin-react-hooks "exhaustive-deps" rule catches this automatically and should be enabled, not disabled, in any real project — silencing it with a comment to make a warning go away is how this bug ends up shipping.
Mistake 2: The Empty Dependency Array Used as "Run Once" Without Meaning It
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // stale: always adds 1 to the count from mount time
}, 1000);
return () => clearInterval(interval);
}, []);
An empty array genuinely means "this effect's logic should never need to see updated values" — if it reads state that changes, that assumption is usually wrong. The fix here is often the functional updater form, which doesn't need the current value as a dependency at all:
setCount((prev) => prev + 1); // reads the latest value at call time, not closed-over
Mistake 3: Missing Cleanup, Causing Memory Leaks and Duplicate Subscriptions
// Bug: no cleanup — a new interval starts on every dependency change,
// stacking on top of ones from previous renders that never stopped
useEffect(() => {
setInterval(() => tick(), 1000);
}, [tick]);
// Fixed
useEffect(() => {
const id = setInterval(() => tick(), 1000);
return () => clearInterval(id);
}, [tick]);
Anything that sets up an ongoing subscription — an interval, an event listener, a WebSocket connection — needs a returned cleanup function that tears it down, or every re-run of the effect (and every unmount) leaks the previous one instead of replacing it.
Mistake 4: Fetching Data Without Handling the Race Condition
// Bug: if userId changes quickly, an earlier, now-stale request can
// resolve after a later one and overwrite the correct data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Fixed: ignore the result if this effect instance is no longer current
useEffect(() => {
let ignore = false;
fetchUser(userId).then((data) => {
if (!ignore) setUser(data);
});
return () => { ignore = true; };
}, [userId]);
This race condition is invisible in local development (requests usually resolve in the order they were sent, over a fast connection) and shows up in production under real network variance — exactly the kind of bug that's expensive to diagnose later because it doesn't reproduce reliably.
Mistake 5: Using useEffect for Something That Doesn't Need It at All
// Unnecessary: this can just be computed directly during render
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Better: no effect, no extra state, no extra render
const fullName = `${firstName} ${lastName}`;
This is arguably the most common useEffect mistake of all: reaching for it to "sync" one piece of state from another, when the value could simply be computed directly in the render body with no state or effect involved. An effect here costs an extra render (state starts empty, then the effect runs and updates it) for something that could have been correct immediately.
Mistake 6: Object or Array Dependencies Recreated Every Render
// Bug: {} is a new object reference every render, so this effect
// runs on every single render regardless of whether config actually changed
useEffect(() => {
doSomething(config);
}, [{ ...config }]);
// Fixed: depend on primitive values, or memoize the object
useEffect(() => {
doSomething(config);
}, [config.id, config.mode]);
React's dependency comparison is reference equality (Object.is), not a deep comparison — a new object or array literal created inline in the dependency array is never equal to the previous render's, so the effect re-runs every time regardless of the actual values inside it.
A Mental Checklist Before Writing Any useEffect
- Does this value even need to be state, or can it be computed directly during render? If the latter, skip the effect entirely.
- Does every reactive value used inside the effect appear in the dependency array? Trust the eslint exhaustive-deps rule here rather than your own judgment.
- Does the effect start something ongoing (subscription, timer, listener)? It needs a cleanup function.
- Does the effect fetch data based on a value that can change quickly? It needs race-condition handling, or a data-fetching library that already handles this (see React Query).
Chasing down a subtle useEffect bug in a React codebase? Get in touch — I work as a freelance React developer.