I was doing a code review for a colleague when I found it.
The component had five useEffect hooks.
No errors. No warnings in the console. The PM ...
For further actions, you may consider blocking this person and/or reporting abuse
I really liked your framing of useEffect as “synchronization with an external system” instead of a lifecycle replacement. That alone reframes a lot of bad habits. I’ve definitely written effects that were basically “run this after render” without asking why. Your derived state example hit close to home. I’ve done that fullName pattern so many times thinking it was clean. It’s interesting how something that looks declarative actually introduces an extra render and potential flicker.
Do you think this misunderstanding mostly comes from people migrating from class components?
Yeah, 100%. That mental model is inherited from componentDidMount / componentDidUpdate. People map useEffect directly onto those, but React’s docs explicitly define it differently, as synchronization, not lifecycle management .
The tricky part is that the old mental model works just enough to pass tests and reviews, so the issues only show up later under stress (like fast navigation or race conditions). That’s why these bugs feel so “mysterious.”
That “works just enough” point is so accurate. I also liked your “one effect, one responsibility” rule. I’ve seen massive effects handling fetches, subscriptions, and DOM updates all in one place, and debugging them becomes a nightmare. But I’ve also heard some devs argue that splitting effects can make things harder to follow. How do you balance readability vs separation?
Good question. I think it comes down to coupling vs cohesion. When you group unrelated concerns into one effect, you’re coupling them artificially. Splitting them might increase the number of hooks, but each becomes easier to reason about independently.
Also, splitting naturally forces you to define proper dependencies and cleanups, which is where a lot of bugs hide. In my experience, more smaller effects = less cognitive load over time.
The stale closure section was probably the most important part for me. The eslint-disable example is something I’ve literally seen in production code. It’s almost treated as a “fix,” when it’s actually hiding the problem. The explanation that the effect “remembers” the initial values forever really clicked. I feel like this is one of those concepts that’s obvious once explained but hard to internalize at first.
Exactly. It’s subtle because nothing crashes, the app just behaves slightly wrong, and those bugs often only show up under specific conditions.
The linter rule (react-hooks/exhaustive-deps) is actually trying to protect you from that, so disabling it is usually a design smell, not a solution . If dependencies feel “annoying”, it usually means the logic belongs somewhere else (like a custom hook or event handler).
As a React developer, this article hit way too close to home.
I’ve reviewed codebases where
useEffectwasn’t just overused, it basically became the app’s unofficial state machine. Everything flowed through it: data fetching, derived values, UI flags, even user actions. And just like described in the article, nothing looked broken… until it was. Subtle flickers, race conditions, ghost state — the kind of bugs that make you question your sanity at 2am.What I really appreciate here is the shift in mental model:
useEffectis for synchronization with external systems, not for orchestrating your app’s logic. That single idea explains most of the five anti-patterns discussed.The “derived state in useEffect” trap
I’ve personally written code like:
At the time it felt clean. In reality, it introduces an unnecessary render cycle and opens the door to UI inconsistencies. The article calls this out perfectly — compute during render, don’t synchronize what’s already inside React.
And honestly, once you unsee this, you start spotting it everywhere. Even in “senior-level” codebases.
Overloaded effects = hidden coupling
The “one effect doing three unrelated things” example is another classic. I’ve seen effects that fetch data, set document title, and wire up subscriptions all together — and then someone adds a dependency and suddenly everything re-runs for no clear reason.
This is where bugs become emergent behavior. Not obvious, not reproducible, just… weird.
Splitting effects by responsibility isn’t just cleaner — it’s the difference between predictable and chaotic systems.
The eslint-disable anti-pattern 🚨
If I see:
I already know there’s a design issue.
The stale closure problem mentioned in the article is real and nasty: your effect silently captures outdated values and keeps using them forever.
And the worst part? It often “works” until it doesn’t.
Data fetching: where things get dangerous
The race condition example is 🔥 because it’s so common and so misunderstood. Two quick state changes, two requests, wrong response wins — boom, inconsistent UI.
This is exactly why I’ve become a big fan of libraries like React Query / TanStack Query. Not because they’re trendy, but because they encode the correct mental model:
The uncomfortable truth: you probably don’t need that effect
This was the biggest mindset shift for me.
A lot of effects are just:
Even the React docs push this idea now, and the article reinforces it well: if you’re not syncing with something outside React, question why the effect exists.
React 18 made everything more obvious (and painful)
Strict Mode double-invoking effects exposed so many hidden issues in legacy codebases. And yeah, it’s annoying at first — but it’s also brutally honest.
If your effect breaks when run twice, it was already broken. The article nails this point.
Final thought from my experience
The biggest React code quality improvements I’ve ever seen didn’t come from adding patterns — they came from removing unnecessary
useEffects.Less synchronization → fewer edge cases
Fewer effects → fewer race conditions
Simpler data flow → easier reasoning
Or said differently:
Curious if others had the same “aha” moment when they stopped treating
useEffectlikecomponentDidMount2.0.Hey @paolozero, this is a fantastic reflection. You basically described the exact journey that pushed me to write the article in the first place.
On useEffect becoming a “state machine”
What you said here is spot on. When everything flows through useEffect, the component stops being declarative and starts behaving like an imperative system hidden inside React. It often feels fine at the beginning, then complexity builds up and the bugs you mentioned start appearing.
The tricky part is that React does not stop you from doing this. It quietly allows it, which is why so many teams fall into the pattern.
Derived state
Your example with fullName is perfect. It looks harmless, even clean, but it introduces an extra render and unnecessary synchronization.
What I have noticed is that developers often reach for useEffect when they feel the need to “react” to something. The shift is realizing that React already reacts during render. If the data is already there, we should compute it directly.
Splitting effects
I am really glad you highlighted this. Mixing concerns inside a single effect is one of the biggest sources of unpredictable behavior.
Each effect should answer one simple question: what external thing am I synchronizing with?
If the answer contains more than one concern, it is usually a sign that the effect should be split.
Disabling the lint rule
Yes, this is often a smell. Not always wrong, but in most cases it hides a deeper issue.
The stale closure problem is subtle because it does not fail loudly. It just drifts away from the current state over time. That makes it hard to debug and even harder to notice during reviews.
Data fetching and race conditions
I completely agree with your take on libraries like TanStack Query. The core issue is that useEffect is too low level for managing async state correctly.
Handling cancellation, caching, and consistency manually inside effects quickly becomes complex. Libraries help because they encode best practices and remove a lot of footguns.
“You probably don’t need that effect”
This is the key idea I hoped people would take away.
Many effects exist because of habit, not necessity. Once you start questioning each one, the codebase becomes simpler and more predictable.
React 18 and Strict Mode
You are right, the double invocation exposed many hidden issues. It can be frustrating, but it is also a great diagnostic tool.
If an effect cannot handle being run more than once, it is often a sign that it is doing too much or doing the wrong thing.
Final note
I really like how you framed it: improvements often come from removing effects rather than adding patterns.
That is exactly the mindset shift. useEffect is powerful, but it should be treated as a last mile tool for synchronization, not the foundation of your logic.
Thanks again for such a thoughtful comment. It adds a lot of depth to the discussion.
Really enjoyed this. The point about useEffect being about synchronization instead of “run this when X changes” hit hard. I think most of us still carry the old lifecycle mental model.
Exactly, that mental shift is the whole game. Once you stop thinking in lifecycle terms, a lot of those “mysterious bugs” just disappear.
The derived state example (fullName) is painfully relatable. I’ve written that exact pattern dozens of times. It feels clean until you realize it adds an extra render cycle and can even flash stale data.
Yeah, it’s one of those patterns that looks harmless but compounds over time. Removing it often simplifies both performance and readability.
I also liked the “one effect, one responsibility” idea. I’ve definitely seen (and written) those giant effects doing fetch + subscriptions + DOM updates all together.
Those are the hardest to debug too.
Splitting them not only improves clarity, but also makes cleanup logic much more obvious.
Recognizable patterns! I’d just like to add something regarding the “Missing or Suppressed Dependencies” antipattern, it’s surely correct that we shouldn’t suppress dependencies (though the
eslint-disable-next-lineESLint rule is familiar to every React developer 😄), there are cases where a dependency technically should be included, but from the logic percpective shouldn’t rerun the effect.That’s exactly the kind of situation React introduced
useEffectEventfor, it lets you extract that dependency from the effect while still accessing its latest value.Absolutely agree, and that’s a great addition!
"useEffectEvent" is exactly the kind of tool that helps solve the “I need the latest value, but I don’t want this effect to resubscribe/re-run” dilemma without fighting the linter or hiding dependencies. React introduced it precisely to separate reactive synchronization from event-like logic.
A lot of developers still reach for refs or "eslint-disable-next-line", so it’s good to highlight that there’s now an official pattern for these cases 😄
Its better to use an abort controller signal when you want to cancel a http request: developer.mozilla.org/en-US/docs/W...
And I would disagree that tan stack query is the most valuable thing you can install. We've actually had many issues with tan stack query in production, because it lacks any kind of middleware or plugin architecture, adding things like authentication or custom logging or error handling becomes a big mess after a while.
Good points, you’re absolutely right about "AbortController". That’s the cleaner and more modern approach for canceling fetch requests compared to the old “ignore stale response” pattern. Definitely something worth using whenever the API supports signals.
And fair criticism regarding TanStack Query too. I still think it brings huge value for caching, synchronization, retries, and request state management, especially compared to hand-rolled "useEffect" fetching. But I agree that once applications become large and need cross-cutting concerns like auth flows, centralized logging, or custom error pipelines, the lack of a true middleware architecture can become noticeable.
In the end, I see it less as “the perfect solution” and more as “a massive improvement over manual effect-based data fetching”.