Articles › React
10 useEffect Mistakes in React and How to Fix Them
The most common useEffect bugs — infinite loops, stale closures, race conditions, missing cleanups and unnecessary effects — with clear fixes and better alternatives.
useEffect is the most misused hook in React. Many bugs — infinite loops, flickering UI, duplicated requests, memory leaks — come from effects that shouldn't exist or are written incorrectly. This article walks through ten real mistakes and how to fix each one.
First, the mental model: an effect synchronizes your component with an external system. If there's no external system (network, DOM API, timer, subscription, third-party library), you probably don't need an effect.
1. Deriving state in an effect
// ❌ Extra render, extra state, possible inconsistency
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ Calculate during render
const fullName = `${firstName} ${lastName}`;
If the calculation is expensive, wrap it in useMemo — still no effect needed.
2. Infinite loops
// ❌ Sets state that is also a dependency → runs forever
useEffect(() => {
setCount(count + 1);
}, [count]);
An effect that updates its own dependency re-triggers itself. Ask why the state should change. Usually it belongs in an event handler, or you need the updater form with a different trigger.
3. Objects and functions as dependencies
// ❌ `options` is a new object on every render → effect runs every render
const options = { includeArchived: false };
useEffect(() => {
fetchItems(options);
}, [options]);
Fixes, in order of preference:
// ✅ Move the object inside the effect
useEffect(() => {
const options = { includeArchived: false };
fetchItems(options);
}, []);
// ✅ Depend on primitive values
useEffect(() => {
fetchItems({ includeArchived });
}, [includeArchived]);
// ✅ Memoize if it must be shared
const options = useMemo(() => ({ includeArchived }), [includeArchived]);
4. Lying about dependencies
// ❌ Uses userId but omits it — keeps showing the first user's data
useEffect(() => {
loadProfile(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Disabling the linter hides a real bug. Include every reactive value the effect reads. If that causes too many re-runs, restructure the code — don't lie to React.
5. Race conditions when fetching
When userId changes quickly, responses can arrive out of order and the old response overwrites the new one.
// ✅ Ignore stale responses with AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") setError(err);
});
return () => controller.abort();
}, [userId]);
The cleanup aborts the previous request whenever userId changes or the component unmounts.
6. Missing cleanup
Every subscription needs an unsubscription:
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
Without cleanup you leak listeners, keep timers running after navigation and update state on unmounted components.
7. Stale closures in intervals
// ❌ `count` is frozen at 0 inside the interval
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// ✅ The updater always receives the latest value
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);
For callbacks that need the latest props without restarting the effect, store them in a ref (or use the newer useEffectEvent where available).
8. Using effects for user events
// ❌ Submitting a form through an effect
useEffect(() => {
if (submitted) {
post("/api/orders", form);
}
}, [submitted]);
// ✅ Do it in the event handler, where it happens
async function handleSubmit(e) {
e.preventDefault();
await post("/api/orders", form);
}
Rule: code that runs because the user did something belongs in an event handler. Code that runs because the component is displayed belongs in an effect.
9. Resetting state when a prop changes
// ❌ Effect-based reset causes a flash of old content
useEffect(() => {
setComment("");
}, [postId]);
// ✅ Give the component a key — React creates a fresh instance
<CommentBox key={postId} postId={postId} />
Changing the key tells React it's a different component, so all of its state resets automatically.
10. Chains of effects
// ❌ Effect A sets state that triggers effect B that sets state that triggers effect C…
useEffect(() => { if (country) setCity(null); }, [country]);
useEffect(() => { if (city === null) setDistrict(null); }, [city]);
Chains cause multiple renders and are hard to follow. Compute everything in one event handler:
function handleCountryChange(next) {
setCountry(next);
setCity(null);
setDistrict(null);
}
Bonus: why does my effect run twice?
In development, StrictMode intentionally mounts, unmounts and remounts components to reveal missing cleanups. If running twice breaks your app, your effect isn't idempotent or is missing a cleanup. Production runs it once.
Where data fetching really belongs
Hand-written fetch effects must handle loading, errors, caching, race conditions and refetching. Libraries like TanStack Query or framework loaders solve all of that:
const { data: user, isLoading, error } = useQuery({
queryKey: ["user", userId],
queryFn: ({ signal }) => fetch(`/api/users/${userId}`, { signal }).then((r) => r.json()),
});
Checklist before writing an effect
- Can I calculate this during render? → no effect.
- Is it a response to a user action? → event handler.
- Do I need to reset state when a prop changes? →
key. - Am I synchronizing with something outside React? → effect, with cleanup.
Summary
Most useEffect problems come from using it for things that aren't side effects. Derive values during render, handle user actions in event handlers, reset state with keys, and keep real effects small, honest about their dependencies and always cleaned up. Your components will become simpler, faster and far easier to debug.
FAQ
Is it bad to have many effects in one component?
Not by itself. Separate effects for unrelated concerns are better than one big effect. But many effects often signal logic that belongs elsewhere or in a custom hook.
useEffect or useLayoutEffect?
Use useEffect by default. Use useLayoutEffect only when you must measure or change the DOM before the browser paints, to avoid visual flicker.
Can I make the effect callback async?
Not directly — an effect must return a cleanup function or nothing. Define an async function inside the effect and call it.