
Lessons Learned Switching from useEffect to TanStack Query
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:
useEffecthas 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.