You Probably Don't Need That useEffect
I do a lot of code review. And the single pattern I flag more than anything else (more than missing keys, more than prop drilling, more than inconsistent naming) is unnecessary useEffect calls. It's everywhere. Junior devs write it, senior devs write it, and AI tools generate it constantly. That last one is the one that really gets me.
If you've searched "React useEffect mistakes 2026," you're already suspicious. Good. Your suspicion is warranted.
useEffect is React's most misused hook. Not because it's complicated in theory (the docs are actually clear) but because it looks like a solution to almost every problem. State that needs to update when something changes? Throw it in a useEffect. Data that needs fetching when the component mounts? useEffect. Something that needs to happen "after render"? useEffect.
The problem is that most of those cases are wrong. The React team has a whole documentation page called "You Might Not Need an Effect", and it covers the exact patterns I keep seeing in production codebases. This post goes through the worst offenders and shows what to do instead.
The Derived State Trap
This is the pattern I see most. Someone has two pieces of state and they want a third value derived from them:
// Don't do this
const [todos, setTodos] = useState(initialTodos);
const [filter, setFilter] = useState('all');
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(todos.filter(t => filter === 'all' || t.status === filter));
}, [todos, filter]);
This causes two renders for every change. The user updates the filter, React renders once with the old visibleTodos, the effect runs, sets the new visibleTodos, and React renders again. You're doing double the work for no reason.
The fix is to just compute the value during render:
// Do this instead
const [todos, setTodos] = useState(initialTodos);
const [filter, setFilter] = useState('all');
const visibleTodos = filter === 'all'
? todos
: todos.filter(t => t.status === filter);
That's it. One line. No useEffect, no extra state, no double render. If the computation is expensive, wrap it in useMemo. But most of the time you don't even need that. The React Compiler, which reached stable in late 2025, handles automatic memoization for most cases now.
I've flagged this in code review probably 50 times in the last two years. The thought process behind it makes sense: "When todos or filter changes, I need to update visibleTodos." That's reactive thinking, which feels natural. But React components are just functions. If todos and filter are in scope, you can compute visibleTodos right there, and it will always be up to date.
Using useEffect to Respond to Events
This one is subtler and I think it's actually harder to catch because it looks more like legitimate "effect" code.
Here's the pattern: someone wants to do something in response to a user action, but instead of putting the logic in the event handler, they update state and react to the state change in an effect:
// Don't do this
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
post('/api/register', { firstName, lastName });
setSubmitted(false);
}
}, [submitted]);
function handleSubmit(e) {
e.preventDefault();
setSubmitted(true);
}
This is roundabout. You need state just to trigger the effect, then you reset the state after the effect runs. It adds two extra state updates and makes the logic much harder to follow.
The right place for this code is the event handler:
// Do this instead
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}
The rule here is straightforward: if code runs because of a specific user action, it belongs in the event handler. Effects are for code that runs because of rendering: syncing with an external system, setting up a subscription, that sort of thing. A form submission is an event. It has a clear cause and a clear moment.
I've seen variants of this pattern with notifications too. Someone updates a cart and wants to show a toast, so they watch the cart state in an useEffect and call showToast() inside it. But this breaks if the component re-mounts (the toast fires again) or if the state gets set from multiple places. Put it in the event handler that actually triggers the cart update and the problem disappears.
Fetching Data in useEffect
This is the big one. It's been taught in tutorials for years. It's in countless blog posts. And it's genuinely bad.
Here's the version everyone writes first:
// Don't do this
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
The immediate problem here is race conditions. If userId changes quickly (say the user clicks through a list) you'll get multiple in-flight requests and no guarantee they resolve in order. The last one to settle wins, which might not be the most recent one.
The second problem is that this pattern gives you no caching, no deduplication, and no automatic retry. Every mount re-fetches. Every navigation re-fetches. It burns network requests on data you might already have.
The fixed-up version with cleanup helps a bit:
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!ignore) setUser(data);
});
return () => { ignore = true; };
}, [userId]);
But this is still a lot of boilerplate just to get to the starting line. And you're still rebuilding caching and deduplication from scratch every time.
The real fix is to use a library for it. TanStack Query (still called React Query by most people) handles all of this for you. The same fetch looks like this:
// Do this instead
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
Caching, deduplication, race condition handling, background refetch, retry. All built in. SWR from Vercel is a lighter-weight alternative with the same idea. And if you're on Next.js, you probably shouldn't be client-fetching most of this data at all. Server components fetch data on the server where it belongs.
The rule the React docs give is a good one: if you're not syncing with a system outside React, you probably don't need an effect. An API is outside React, so fetching does qualify technically. But the implementation matters, and raw useEffect fetching is almost always the wrong implementation.
The componentDidMount Trap
This one comes from muscle memory. Before hooks, you'd put initialization logic in componentDidMount. With hooks, people reach for useEffect with an empty dependency array and assume it's the same thing:
// This is not componentDidMount
useEffect(() => {
analytics.init();
checkAuthToken();
}, []);
There are two problems. First, in React 18 with Strict Mode enabled (which is default in development with frameworks like Next.js), effects run twice. Your analytics library initializes twice. Your auth token check fires twice. This is intentional. React is checking that your effects are idempotent and that cleanup works. But it catches a lot of code that assumed this would only run once.
Second, if this logic truly only needs to run once for the whole app (not per component mount), it shouldn't be in a component at all. The React docs recommend running it at module level:
// This actually only runs once
if (typeof window !== 'undefined') {
analytics.init();
checkAuthToken();
}
function App() {
// ...
}
This runs when the module loads, not on every render, and it's not subject to the Strict Mode double-invocation. For real app initialization that should happen exactly once, this is the right place for it.
What useEffect Is Actually For
After all these anti-patterns, I want to be clear: useEffect does have real use cases. The React docs describe it as a way to synchronize your component with an external system, something outside React that can't be driven by rendering alone.
Good uses of useEffect:
- Setting up and tearing down event listeners (
window.addEventListener/ cleanup) - Connecting to a WebSocket or chat server and disconnecting on unmount
- Calling
.play()or.pause()on a<video>element to sync it with a prop - Initializing a third-party map widget that manages its own DOM
- Firing analytics page view events (though even this needs care)
The pattern that unifies all of these: you're talking to something that exists outside React's rendering model. The browser's DOM API, a server connection, a third-party library that controls its own state. That's when effects make sense.
If you're only reading and transforming data that already lives in React state or props, you don't need an effect. Compute it during render.
Why AI Gets This Wrong
I'll be direct here: every AI coding tool I've used generates the useState + useEffect derived state pattern regularly. I catch it in code review when someone uses Copilot or ChatGPT to scaffold components. The tools have read every React tutorial from 2018 to 2022 and that's where this pattern was most popular in writing, so it's in the training data, so it comes out.
The React team's framing has actually shifted since then. The hooks documentation now leads with the mental model of effects as "escape hatches" for external systems, not general-purpose lifecycle methods. But the old mental model is deeply embedded in LLM outputs and, honestly, in a lot of engineers' heads too.
The practical upshot: when you see a useEffect in code (yours, a colleague's, or anything an AI wrote) ask one question first: is this syncing with something external to React? If the answer is no, there's almost certainly a better way.
Recommended Resources
- You Might Not Need an Effect, The React docs page this post is partly based on. Worth reading in full.
- Synchronizing with Effects, The counterpart that explains what effects are actually for.
- TanStack Query, The library to use for server state. Don't write data fetching in
useEffect. - SWR, Vercel's lighter alternative to TanStack Query, great for simpler apps.