useEffect mistakes to avoid in your React apps
The React community seems divided on the useEffect Hook. Often you will hear things like “Stop using useEffect!” – as if it’s a bad Hook. But the truth is, the problem is not with the Hook itself, but with how developers use (and overuse) it.
useEffect has been abused so much that it’s now associated with unnecessary re-renders, wasted computations, and slow apps.
So let’s fix that. We’ll break down the confusion, clear up misconceptions, and go over 15 common mistakes I’ve seen repeatedly in React apps I’ve reviewed.
If you’re a dev leader, there’s a good chance your team might be making some of these common useEffect mistakes. There’s a lot of confusion in the React community around when and how to use useEffect, so it’s easy for issues to slip into production code.
A good rule of thumb: useEffect should be the exception, not the default. If you see it popping up often, ask whether it’s really the right tool for the job.
Share this post with your team so that it can help them understand when useEffect is necessary and when it’s being misused.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
useEffect: Unlearning old patternsOne of the biggest sources of confusion comes from comparing React Hooks to the lifecycle method in class components.
But there’s a critical distinction. Many developers mistakenly believe that useEffect equals componentDidMount + componentDidUpdate. This is wrong.
componentDidMount and componentDidUpdate run synchronously after DOM updates but before browser paint.useEffect runs after the browser has painted the screen.The key distinction is that useEffect is non-blocking and runs asynchronously after paint, while class lifecycle methods run synchronously. This timing difference is crucial. If you use useEffect to run effects that should happen before paint, your effects may run too late, causing visual flickers or layout issues.
In short: useEffect is not the same as componentDidMount/Update.
useEffectEvery time you consider using useEffect, ask these three fundamental questions:
useEffect is designed to synchronize your React component with external systems. What counts as an external system?
localStorage, geolocationRule: If you do not need an external system, you probably don’t need useEffect.
If an external system is involved, you must determine the trigger: what should cause this effect to run?
useEffect mistakesI reviewed several open-source projects, including those created for personal or side projects, as well as community-led initiatives. This helped me identify patterns in how useEffect and other Hooks are used. I’ve noticed some common anti-patterns. Let’s break them down:
This occurs when a state update is triggered inside an effect, and that effect is not controlled by a dependency array, causing the component to perpetually re-render. This results in an “infinite render problem.”:
// ❌ DON'T: Causes infinite loop
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Triggers re-render → runs effect → triggers re-render...
}); // No dependency array!
return <div>{count}</div>;
}
The Fix: Add an empty dependency array to run only on mount:
// ✅ DO: Runs only once on mount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []); // Empty array = run once on mount
return <div>{count}</div>;
}
The Problem: Reading state inside an effect that was just updated causes unnecessary renders and shows stale values.
Example:
// ❌ Mistake: reading stale 'user' right after setting it
function Component() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
console.log("Fetched user:", user); // stale value (still null!)
});
}, []);
return <div>{user.name}</div>;
}
✅ The Fix: Use the data directly or react to changes properly
function Component() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!isMounted) return;
setUser(data);
});
}, []);
// ✅ Log when 'user' actually updates
useEffect(() => {
if (user) {
console.log("Fetched user:", user);
}
}, [user]);
return <div>{user.name}</div>;
Now:
console.log runs only after user has updated.When fetching data based on props, omitting that prop from the dependency array means the effect will only run once on mount, regardless of changes to the prop. This can cause stale data:
// ❌ DON'T DO THIS
function Temp2({ userId }) {
// ...
useEffect(() => {
fetchUser(userId);
}, []); // Missing userId dependency!
// ...
}
If the userId prop changes, the effect will not re-run, and the component will be stuck using the stale data fetched with the original userId. The fix is simple: add userId to the dependency array.
The Fix: Add any props you need to the dependency array:
// ✅ DO THIS
function Temp2({ userId }) {
// ...
useEffect(() => {
fetchUser(userId);
}, [userId]); // Fixed userId dependency!
// ...
}
Please note: These errors are easy to avoid by enabling the eslint plugin: eslint-plugin-react-hooks
useEffects for chained reactionsIf multiple effects react to the same dependency, they should be combined into a single effect:
// ❌ DON'T DO THIS: Multiple useEffects with same dependency
function Temp2({ userId }) {
// ...
useEffect(() => {
fetchUser(userId);
}, [userId]);
useEffect(() => {
getProfilePicture(userId);
}, [userId]);
useEffect( () => {
setLoginInfo(userId);
}, [userId]);
}
Each effect above runs whenever userId changes, meaning React has to schedule and clean up three separate effects for the same trigger.
Instead, combine them into one:
// ✅ DO THIS: Single effect for related dependencies
function Temp2({ userId }) {
// ...
useEffect(() => {
fetchUser(userId);
getProfilePicture(userId);
setLoginInfo(userId);
}, [userId]);
}
This reduces overhead, makes your code easier to reason about, and ensures all logic responding to userId updates happens together.
Side note: if effects are unrelated (e.g. one updates analytics, another subscribes to a WebSocket), keep them separate for clarity.
Objects and functions created during render are treated as new references by JavaScript on every render, even if their contents haven’t changed. Including them in the dependency array will cause the effect to re-run unnecessarily, leading to excessive data fetching:
// ❌ DON'T DO THIS: User is an unstable object reference
const user = { userId: 123, profile: {}};
useEffect(() => {
fetchUser(user.userId);
}, [user]);
To fix this, only pass the stable primitive properties you need, such as user.id. Instead of adding the entire object in your dependency, only add the actual property that you need:
// ✅ DO THIS: Use primitive value
const user = { userId: 123, profile: {}};
useEffect(() => {
fetchUser(userId);
}, [userId]);
If you do need to add the function, you should memoize it using useCallback:
// Use useCallback for unstable function references
const fetchUserProfileMemoized = useCallback(
() => {
// fetch logic
},
[
/* dependencies */
],
);
useEffect(() => {
fetchUserProfileMemoized();
}, [fetchUserProfileMemoized]);
Note: With React Compiler enabled in modern React versions (React 17, 18, 19), the compiler can handle this automatically.
useEffectThis is perhaps the most frequent misuse of the Hook. If you need to update one piece of state based on changes to props or another piece of state, you should derive it directly during rendering.
For instance, calculating a fullName from firstName and lastName inside an effect is considered redundant and unnecessary:
// ❌ AVOID: Redundant state and unnecessary effect
function EventTeam({ isOnTeam }) {
const [onTeam, setOnTeam] = useState(isOnTeam);
useEffect(() => {
setOnTeam(isOnTeam);
}, [isOnTeam])
}
The correct approach is to calculate it during rendering:
// ✅ CORRECT: Calculated during rendering. Does not need a useState or useEffect const onTeam = isOnTeam;
Sometimes, we need a value to be reactive to prop change. It is a common mistake to add an additional dependency on useEffect to ensure it re-runs on prop change:
// ❌ AVOID: Doesn't need a useEffect
function EventTeam({ isOnTeam, isHelping }) {
const [onTeam, setOnTeam] = useState(isOnTeam);
useEffect(() => {
setOnTeam(isOnTeam);
}, [isOnTeam, isHelping])
});
Since the component re-renders when prop changes, we do not need a useEffect for this:
// ✅ FIX: Calculate isOnTeam directly from props.
function EventTeam({ isOnTeam, isHelping }) {
const onTeam = isOnTeam;
});
A common mistake is to use useEffect to reset state variables when certain props change:
// AVOID: Unnecessary state reset inside effect
function Temp2({ hackathon }) {
const [judgeDetailsCache, setJudgeDetailsCache] = useState([]);
const [panelAssignmentsCache, setPanelAssignmentsCache] = useState([])
// Problem: This causes an extra render and temporary inconsistent state
useEffect(() => {
// Clear caches when hackathon changes
setJudgeDetailsCache([]);
setPanelAssignmentsCache([]);
}, [hackathon]);
// Fetch judge details...
useEffect(() => {
fetchJudgeDetails(hackathon.id).then(setJudgeDetailsCache);
}, [hackathon.id]);
}
Here, the intention is that each hackathon should have its unique judgesDetails and panelAssisgments
Fix: You can use a key to let React know that each hackathon.id is a different and unique component:
<Temp2 key={hackathon.id} hackathon={hackathon} />
React treats different keys as different components. When hackathon.id changes, React unmounts the old Temp2 instance and mounts a new one. New mount means fresh state, therefore useState initializers run again with empty arrays.
With this, you can do:
// ✅ GOOD: Let React handle the reset via key
function Temp2({ hackathon }) {
const [judgeDetailsCache, setJudgeDetailsCache] = useState([]);
const [panelAssignmentsCache, setPanelAssignmentsCache] = useState([]);
// No manual reset needed! State starts fresh when key changes
useEffect(() => {
fetchJudgeDetails(hackathon.id).then(setJudgeDetailsCache);
}, [hackathon.id]);
return (
<div>
{judgeDetailsCache.map(judge => <div key={judge.id}>{judge.name}</div>)}
</div>
);
}
⚠️ Caveat:
The key approach remounts the entire component, which means:
This is usually what you want when a prop fundamentally changes the component’s identity. If you only need to reset some state while preserving other state, then useEffect might still be appropriate
When useEffect is used to subscribe to external systems such as APIs, DBs, and WebSocket connections, failing to properly handle cleanup for external system interactions can lead to memory leaks and unexpected behavior.
When performing data fetching, failing to cancel the request if the component unmounts while the request is still pending can lead to stale fetch requests.
To fix this, you must use an AbortController and return a cleanup function that aborts the request:
useEffect(
() => {
const controller = new AbortController();
fetch(URL, { signal: controller.signal });
// ... then continue with response handling
return () => {
controller.abort(); // Cleanup function cancels the request on unmount/re-render
};
},
[
/* dependencies */
],
);
Even with the AbortController, if you update state upon request completion, such as setting loading to false, you might still risk a memory leak if the component unmounts right before the response is processed.
To fix this, use a mutable flag, like isMounted, managed within the cleanup function:
useEffect(() => {
let isMounted = true;
// ... fetch logic ...
.then(data => {
if (isMounted) {
setNewsData(postsData);
// ... set other states
}
});
return () => {
isMounted = false; // Set to false on cleanup
// ... abort controller logic
};
}, [/* dependencies */]);
This way, you check to make sure that the component is mounted before we call any set state[s].
Suppose your effect adds an external event listener (e.g., to the window or document). In that case, you must remove it in the cleanup function to prevent it from leaking memory and causing unintended side effects across your application:
useEffect(() => {
window.addEventListener('resize', onResize);
return () => {
// Make sure we remove event listener on unmount
window.removeEventListener('resize', onResize);
};
}, [onResize]);
useEffect for things it’s not supposed to be used foruseEffect for event-specific logicIf you use useEffect to watch for a state change that was triggered by a user action (like showing a notification when an item is added to a cart), you’re misapplying the Hook:
// ❌ AVOID: Event-specific logic inside an effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product.isInCart]);
This logic belongs directly in the event handler function that triggered the cart change.
If you initialize an entire third-party library or service that should only run once per application load, putting that logic in every component via useEffect is inefficient:
// ❌ AVOID: Initializing app-level services in component effects.
useEffect(() => {
initFacebookPixel();
mermaid.initialize({...});
}, []);
Instead, this global initialization should be moved to a single high-level component, such as App.tsx, and wrapped with a flag to ensure it only runs once per session:
// ✅ FIX: Initiliaze once at App level after checking if initialized before
//App.tsx
useEffect(() => {
if(!isAppInitialized) { //isAppInitialized should be a singleton
initFacebookPixel();
mermaid.initialize({...});
}
}, []);
useEffect is not the right hook for the jobRemember, useEffect runs after the browser paints. If you are dealing with DOM manipulation or measurement, such as calculating a tooltip’s position, you need to run the logic before the visible paint step to avoid flickering.
In these specific scenarios, using useLayoutEffect is the correct approach. useLayoutEffect is a version of useEffect that fires before the browser repaints the screen.
Similarly, Hooks such as useSyncExternalStore, are made specifically meant for working with external stores. This Hook is better than useEffect, because useSyncExternalStore ensure that the subscription callback is only fired once.
These specialized hooks are often better than useEffect.
useEffectEvent for non-Reactive logic (React 19.2+)For complex scenarios like tracking analytics, where you need to read the latest props/state values without forcing the effect to re-run every time those values change, React has introduced useEffectEvent.
The useEffectEvent Hook lets you extract non-reactive logic from your Effects into a reusable function called an Effect Event.
By wrapping the tracking logic in useEffectEvent, you ensure that the event handler function always reads the latest props (like step and enhancedMetadata) without needing to add them to the effect’s dependency array. This prevents needless re-runs while maintaining data integrity.
Fixing these common mistakes will lead to cleaner, faster, and more maintainable React applications. While data fetching libraries offer solutions for many of the cleanup and caching problems associated with useEffect, understanding the core rules of the Hook is essential for mastering React development.
Although useEffect is a commonly misused Hook, helping teams understand its common pitfalls can prevent mistakes and improve existing code. Developer leaders can use these insights to guide their teams, foster better coding practices, and ensure Hooks are used effectively across projects.

MCP is the bridge between AI and the open web — giving intelligent agents the ability to act, not just talk. Here’s how this open protocol transforms development, business models, and the future of software itself.

AI agents can now log in, act, and access data, but have you truly authorized them? This guide walks through how to secure your autonomous agents using Auth0’s Auth for GenAI, covering token vaults, human-in-the-loop approvals, and fine-grained access control.

A hands-on guide to building an FTC-ready chatbot: real age checks, crisis redirects, parental consent, audit logs, and usage limits – designed to protect minors and prevent harm.

CSS text-wrap: balance vs. text-wrap: prettyCompare and contrast two CSS components, text-wrap: balance and text-wrap: pretty, and discuss their benefits for better UX.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now