DEV Community

Sibasish Mohanty
Sibasish Mohanty

Posted on • Edited on

Stop Fetching Data in useEffect: You’re Not Writing jQuery Anymore

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)
}, [])
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

or in React Router:

const data = useLoaderData()
Enter fullscreen mode Exit fullscreen mode

or, in the future:

const data = await fetchSomething()
Enter fullscreen mode Exit fullscreen mode

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))
})
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
srishtikprasad profile image
Srishti Prasad

@sibasishm Great perspective, really well put !