Lessons Learned Switching from useEffect to TanStack Query
React

Lessons Learned Switching from useEffect to TanStack Query

SI

Shamal Iroshan

2026-01-28 | 5 min read

Why I Stopped Using useEffect for Data Fetching (and Switched to TanStack Query)

For a long time, I used useEffect for data fetching.
Not because it was good - but because it was there.

Every React project started the same way:

useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(setUsers)
}, [])

It looked simple. It worked.
Until the app grew.


The First Crack: Loading States Everywhere

The first real problem showed up when the UI needed to show a loader.

So I added:

const [loading, setLoading] = useState(false)

Then error handling:

const [error, setError] = useState(null)

Now every component fetching data had:

  • loading
  • error
  • data
  • effect logic

Copy paste city.

And worse - every screen handled this slightly differently.


The Second Crack: Duplicate API Calls

Then came a simple requirement:

“Show the same user data in two components on the same page.”

Both components had their own useEffect.

Both fired a request.

Both hit the backend.

Now imagine:

  • dashboards
  • modals
  • side panels
  • tooltips

The same endpoint was being called multiple times, for the same data, in the same render cycle.

At this point, I realized:

useEffect has no idea what the rest of the app is doing.


The Real Pain: “Refresh This After Update”

This is where things really started breaking.

I had a form:

  • Update user profile
  • Go back to list
  • Expect updated data

With useEffect, this meant:

  • Manually triggering a refetch
  • Passing callbacks between components
  • Or lifting state higher and higher

One update required touching:

  • The form
  • The list
  • The parent
  • Sometimes global state

This wasn’t scalable. It was fragile.


Enter TanStack Query

I didn’t adopt TanStack Query because of performance.
I adopted it because I was tired.

The first thing that shocked me was this:

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

No useEffect.
No loading state.
No manual refetch.

And yet… it just worked.


The Moment It Clicked: Cache, Not Fetching

The “aha” moment came when I realized:

TanStack Query is not calling my API - it’s managing my data.

When two components used the same query key:

['users']

They didn’t fetch twice.

They shared data.

This alone removed:

  • duplicate requests
  • inconsistent UI
  • race conditions

Something useEffect could never do.


The Best Part: “Just Invalidate It”

Remember the “refresh after update” nightmare?

With TanStack Query, it became this:

queryClient.invalidateQueries({ queryKey: ['users'] })

That’s it.

No callbacks.
No prop drilling.
No manual state syncing.

The list refetched itself only when needed.

This was the moment I stopped reaching for useEffect.


Loading Without Blocking the UI

One thing I hated with useEffect:

  • Loading spinners everywhere
  • UI freezing for no reason

TanStack Query showed me a better way.

Even when data was “stale”, the UI:

  • kept showing old data
  • fetched new data silently
  • updated when ready

Users didn’t care that the data was 10 seconds old.
They cared that the app felt fast.

This is something I never achieved with manual fetching.


The “Why Is This Refetching?” Problem - Solved

With useEffect, when something refetched unexpectedly:

  • I had no visibility
  • No idea why
  • No easy way to debug

TanStack Query DevTools changed that completely.

I could see:

  • when data became stale
  • why it refetched
  • who triggered it

This alone saved hours of debugging.


Infinite Scroll Without State Hell

It had:

  • page counters
  • concatenated arrays
  • race conditions
  • duplicate items
  • broken scroll positions

With TanStack Query:

useInfiniteQuery()

It handled:

  • page caching
  • next page logic
  • refetching correctly

The difference was night and day.


Error Handling That Makes Sense

With useEffect, every fetch had:

  • try/catch
  • retry logic (or none)
  • inconsistent UX

TanStack Query gave me:

  • automatic retries
  • global error handling
  • per-query overrides

Now errors behaved predictably.


When I Still Use useEffect

I didn’t stop using useEffect completely.

I still use it for:

  • event listeners
  • subscriptions
  • DOM interactions
  • side effects unrelated to data

But for server data?

Never again.


What TanStack Query Actually Gave Me

Not performance.
Not magic.

It gave me:

  • consistency
  • predictability
  • confidence to scale

I stopped thinking about how to fetch data and started thinking about what data my UI needs.


Final Thoughts

If your app:

  • fetches data from an API
  • updates that data
  • shows it in multiple places

You will eventually rebuild TanStack Query yourself.

The difference is:

  • TanStack Query is already tested
  • battle-hardened
  • and smarter than most homegrown solutions

Switching away from useEffect wasn’t an optimization.

It was growing up as a frontend engineer.



Loading comments...