DEV Community

Sibasish Mohanty
Sibasish Mohanty

Posted on

Stop Fetching Data in useEffect: Redux Edition

In Part 1, we talked about why fetching data directly inside useEffect is a flawed approach — tightly coupling component lifecycle to data-fetching logic, leading to unmanageable side effects, broken UX, and stale state bugs. We compared it to jQuery’s imperative data flows and showed how React in 2025 demands a more declarative mental model.

But the story doesn’t end there.

In this second part, we tackle an even more common anti-pattern, especially in Redux-heavy codebases:
Dispatching async thunks from useEffect.

useEffect(() => {
  dispatch(fetchUserData(userId))
}, [userId])
Enter fullscreen mode Exit fullscreen mode

It might look cleaner than raw fetch(), but it inherits the same rot underneath.

Why This Pattern Still Exists

Before React Hooks, data fetching in Redux apps happened in componentDidMount(), with connected components via connect() HOCs. That lifecycle-to-dispatch model was normative.

When hooks arrived with useEffect, developers naturally ported this over. This became the de facto pattern. Even the official React-Redux docs show this for simple use cases.

It was a one-to-one migration. And at the time, it made sense.

You needed to kick off a thunk or saga somewhere, and useEffect was the hammer for all side-effect nails.

But Redux evolved.

As Mark Erikson (Redux maintainer) has repeatedly emphasized:

"You probably don’t need useEffect. And if you're dispatching from it, it's likely a code smell."

Yet, many codebases (even recent ones) still cling to the old dispatch-in-useEffect dance. Let’s unpack why that’s a problem.

The Core Problem

This pattern:

  • Delays data availability until after first render
  • Forces you to manage loading and error state manually
  • Ties async logic to the lifecycle, not the state graph
  • Doesn’t support SSR or Suspense
  • Is verbose, hard to test, and easy to misuse
  • Encourages repetition (every page/component reimplements fetch logic)

Worse, this makes your components non-composable. You can’t reuse this logic in another component without copy-pasting or lifting.

You're not modeling data — you're modeling timing.

But Isn’t Redux Still Useful?

It is — but not for remote server data.

Redux is great for:

  • Global, non-remote state (auth, UI toggles, preferences)
  • Workflow state (multi-step forms, undo/redo, complex user flows)
  • Cross-cutting logic (feature flags, permission checks)

But for:

  • Fetching lists
  • User profiles
  • Paginated feeds
  • Notifications
  • Dashboard metrics

...classic Redux is too much ceremony.

You don’t need reducers, actions, thunks, and selectors just to load some damn data.

You need a query layer — not a state machine factory.

Is dispatch() in useEffect() Always Wrong?

Not strictly.

Sometimes you genuinely need to dispatch on mount, maybe to initialize a websocket, fire a metrics event, or pre-warm a cache.

But when you're fetching render-critical data, dispatching inside useEffect means you're reacting too late.

You’ve already rendered once.
You’ve already missed the chance to prefetch or SSR.
You’ve already broken the declarative contract.

If You Have to Use Classic Redux

Encapsulate the dispatch logic in a custom hook to decouple data orchestration from your component:

function useUserData(userId) {
  const dispatch = useDispatch()
  const data = useSelector(state => state.users[userId])
  const loading = useSelector(state => state.loading.users[userId])

  useEffect(() => {
    dispatch(fetchUserData(userId))
  }, [userId])

  return { data, loading }
}
Enter fullscreen mode Exit fullscreen mode

Then use declaratively:

const { data, loading } = useUserData(userId)
Enter fullscreen mode Exit fullscreen mode

This way:

  • Your component stays declarative
  • The effect logic is testable and reusable
  • You reduce boilerplate inside the view layer

Still far from ideal — but survivable.

So What Should You Do Instead?

Data fetching deserves better than a useEffect hack job. If your mental model is still stuck in 2019, it's time for a reset.

In Part 3, we’ll explore a *declarative routing-first approach * to data fetching using loader, useLoaderData, and compare that with popular solutions like RTK Query, TanStack Query, swr and even emerging Suspense APIs. We'll also address how to gracefully migrate away from this legacy pattern in large codebases.

And yes — there’s a way to do all this without ditching Redux if you don’t want to.

TL;DR

  • useEffect is not a data-fetching tool
  • Dispatching async thunks in useEffect is an outdated, fragile pattern
  • This made sense in pre-RTK Redux, but should be retired
  • Redux already solved this. It’s called RTK Query.
  • RTK Query, TanStack Query, and Loader APIs offer better abstractions
  • Declarative data > lifecycle-driven orchestration

If you’re still writing async thunks just to get a list of users, it’s time to ask yourself why.

You’re still not writing jQuery.
And now — you shouldn't be writing it with Redux either.

Top comments (4)

Collapse
 
pmbanugo profile image
Peter Mbanugo

I struggle to understand what you're trying to teach in the articles in your series. It kept saying "don't do this", "here's why it's bad".... without any example to proof your point or show a working alternative and why it's better.

You said the following useEffect is bad:

useEffect(() => {
  dispatch(fetchUserData(userId))
}, [userId])
Enter fullscreen mode Exit fullscreen mode

Yet you wrote the same thing at the end, and showed it as the way to do it, but with extra level of abstraction:

function useUserData(userId) {
  const dispatch = useDispatch()
  const data = useSelector(state => state.users[userId])
  const loading = useSelector(state => state.loading.users[userId])

  useEffect(() => {
    dispatch(fetchUserData(userId))
  }, [userId])

  return { data, loading }
}
Enter fullscreen mode Exit fullscreen mode

If dispatching dispatching or fetching in useEffect() is bad, why then do you have it in your better example?

You seem to confuse SSR/Suspense with SPA and tools like Redux and React Query.

RTK Query, TanStack Query, and Loader APIs offer better abstractions

How do they do the data fetching in the client side without useEffect?

But when you're fetching render-critical data, dispatching inside useEffect means you're reacting too late.

You’ve already rendered once.
You’ve already missed the chance to prefetch or SSR.

So for a SPA, how would they fetch data without using useEffect? can you pls show an example without a query selector? if you use a query selector, can you explain how they're able to fetch data without using useEffect?

Collapse
 
sibasishm profile image
Sibasish Mohanty

Thanks for the thoughtful comment and you're absolutely right to call this out.

Let me clarify:

The example with useEffect(() => dispatch(fetchUserData(userId)), [userId]) inside a custom hook isn’t meant to be the ideal pattern, it's meant to mirror legacy approaches, just with encapsulated logic.

But you’re spot on: if render-critical data is fetched after the initial render via useEffect, you're already late. That’s the entire point I’m teasing. This pattern is deeply entrenched in many Redux-based SPAs, but it’s flawed by design.

In Part 3, I’ll contrast this with:

  • how RTK Query / TanStack Query avoid useEffect-based fetch triggers entirely
  • how Loader APIs in frameworks like Remix or Next 14 App Router do SSR-prefetching before hydration
  • how render-blocking fetches using Suspense or preload APIs shift the fetch ahead of render
Collapse
 
pmbanugo profile image
Peter Mbanugo

how RTK Query / TanStack Query avoid useEffect-based fetch triggers entirely

Don't they just hide the useEffect from you? On the client, it'll still happen after first render. Just look at the browser network tab or your query devtools. Here's the base query hook of Tanstack using useEffect link

how Loader APIs in frameworks like Remix or Next 14 App Router do SSR-prefetching before hydration

Goes back to my initial comment that you're confusing SPA vs SSR, and mixing it up with what useEffect is.

how render-blocking fetches using Suspense or preload APIs shift the fetch ahead of render

Render-blocking calls are still render blocking, Suspense just creates an illusion by using static component/ui to avoid showing black screen to your user. They don't improve performance realistically.

I struggle to connect the dot with your series because it's mixing various things

Collapse
 
dariomannu profile image
Dario Mannu

React's Hooks are so poorly designed it's no surprise most tutorials on the Internet start like "don't do this", "you can't do that", etc. 👾