Ah, the good old useEffect
.
Every beginner's favourite foot-gun.
Every senior's silent scream.
Every production codebase’s quiet shame.
You’ve seen it.
You’ve written it.
Hell, it might be on your screen right now:
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
}, [])
Looks innocent enough, right?
Wrong.
This pattern is everywhere. Even though it works, it is wrong in almost every way you can imagine.
It causes bugs, stale data, race conditions, bad UX, leaky abstractions, and often a deeply misunderstood component lifecycle.
Let’s talk about why it’s broken and how you should actually be thinking about data in a React app in 2025.
Mistake #1: Treating useEffect as a Lifecycle Hook
This isn't componentDidMount()
.
React isn’t Angular. And you’re not building lifecycle-controlled spaghetti anymore.
Most devs treat useEffect
as a “do something after render” escape hatch.
That’s what it technically is, sure.
But semantically, it was never meant to control side effects that affect rendering.
React’s mental model has shifted. Yours should too.
Let me repeat:
If you’re fetching data inside useEffect
so you can render a component, you’ve already lost.
Why?
Because now the component’s rendering logic is tied to its mounting lifecycle. That’s an anti-pattern.
Rendering should be pure.
Fetching data is not pure. It’s async, flaky, error-prone, and mutable. Tying it to mount-time means you’ve coupled rendering with a network side-effect.
Mistake #2: You’re Hacking a State Machine Without Realising It
Fetching data inside useEffect
is implicitly creating a mini state machine.
- Start loading
- Fetch fires
- Might succeed
- Might fail
- Maybe you need to cancel it
- Maybe user navigates away mid-flight
- Maybe stale data renders before fresh one
- Maybe component unmounts during fetch
None of this is accounted for in that happy little fetch snippet.
And you end up duct-taping more useState
, isMounted
, AbortController
, and useRef
logic until your app becomes a spaghetti of async half-states. Most importantly, it makes your component non-composable.
All because you didn’t treat data as a stateful, queryable entity and requests as transitions. Remember, data fetching is just another declarative concern — like styling or routing.
Mistake #3: You’re Opting Out of Suspense
React is evolving toward data-first rendering. — not effect-first.
When you fetch in useEffect, you opt out of:
- Server rendering
- Streaming UI
- Seamless loading states
- Skeleton UIs
- Predictable hydration
And fall into the trap of “loading → render → loading again”.
Which makes your UX janky and fragile.
Meanwhile, with TanStack Query / RTK Query / SWR the pattern is declarative:
const { data } = useQuery(...)
or in React Router:
const data = useLoaderData()
or, in the future:
const data = await fetchSomething()
via use()
and Suspense.
You describe what data your component needs, not when to go fetch it.
Why You’re Still Doing This (Even in 2025)
Let’s be honest — this isn't entirely your fault.
Remember in class components data fetching went in componentDidMount()
. We all learned the same muscle memory.
Then came hooks, and everyone reached for useEffect
as the closest match.
Add to that:
- Redux centralization cults
- Overuse of global state
- Poorly updated blog posts
- Lazy YouTube tutorials to get you started
- StackOverflow copy-pasta
And boom: your brain thinks “fetch = useEffect” without question.
But this is not jQuery.
In jQuery, you used to say:
$(document).ready(() => {
$.ajax(...).done(data => render(data))
})
Sound familiar?
Yeah — useEffect(() => fetch().then(setState))
is just the same pattern in different clothes.
Imperative. Mount-triggered. Not resilient.
And worst of all: completely decoupled from React’s declarative dataflow.
TL;DR — useEffect is not your data-fetching buddy
React in 2025 is about declarative data, not lifecycle effects.
Components describe what UI should look like given state — not how to get the state.
So next time you write a fetch call in useEffect
, ask yourself:
- Is this data truly local to this component?
- Does it affect the rendering?
- Will it need to be cached, paginated, retried, or shared?
If so — you shouldn’t be manually fetching it.
You should be declaring it.
Top comments (1)
@sibasishm Great perspective, really well put !