Stop Fetching Data in useEffect: Redux Edition

posted 3 min read

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])

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 }
}

Then use declaratively:

const { data, loading } = useUserData(userId)

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.

If you read this far, tweet to the author to show them you care. Tweet a Thanks
0 votes
0 votes
0 votes

More Posts

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

Sibasish Mohanty - Jul 23

React Router Data Mode: Part 3 - Loaders and data fetching

Kevin Martinez - Jul 7

Frontend 2025: Make It Fast, Keep It Simple

Vishwajeet Kondi - Sep 2

React Router v7: The Evolution React Needed

Kevin Martinez - Jun 26

Nucleux

MartyRoque - Jun 23
chevron_left