If your React components are littered with useEffect hooks just to fetch data, it’s time to level up your approach.
React has given us a lot of power with useEffect, but with great power comes great... boilerplate.
Let’s face it — using useEffect for data fetching often ends up messy. We handle loading states, errors, cancellations, caching, refetching, and sometimes race conditions... manually. It’s a lot.
Enter TanStack Query (formerly React Query) — a game-changer for handling async data in React apps.
The Problem with useEffect for Fetching Data:
//Here’s what a typical useEffect data fetch looks like import { useState, useEffect } from 'react'; const Users = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const fetchUsers = async () => { try { const res = await fetch('/api/users', { signal: controller.signal }); const data = await res.json(); setUsers(data); } catch (err) { if (err.name !== 'AbortError') setError(err); } finally { setLoading(false); } }; fetchUsers(); return () => controller.abort(); // cleanup }, []); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {users.map(u => <li key={u.id}>{u.name}</li>)} </ul> ); };
This looks okay — until you scale. Then you deal with:
Multiple API calls
Caching concerns
Race conditions
Data syncing
Window refocus or re-fetching logic
Manual error boundaries
You get the idea.
The TanStack Query Way 🚀
Now, here’s the same thing using TanStack Query:
npm install @tanstack/react-query
import { useQuery } from '@tanstack/react-query'; const fetchUsers = async () => { const res = await fetch('/api/users'); if (!res.ok) throw new Error('Network response was not ok'); return res.json(); }; const Users = () => { const { data, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: fetchUsers, }); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {data.map(u => <li key={u.id}>{u.name}</li>)} </ul> ); };
That’s it.
No state management.
No useEffect.
No AbortController.
No complex cleanup logic.
🚀 Why TanStack Query Rocks
Here’s what you get out of the box:
⭐ Automatic caching
⭐ Background refetching
⭐ Pagination and infinite queries
⭐ Optimistic updates
⭐ Retry logic
⭐ Query invalidation
⭐ Devtools for debugging
⭐ SSR support
⭐ Works with any data-fetching library (Axios, fetch, GraphQL, etc.)
💡 When Should You Use TanStack Query?
Use it when:
You’re building anything beyond a simple static app.
You need multiple API calls or shared data.
You want to avoid manually managing loading/error states.
You want a reactive and declarative approach to fetching.
But keep in mind: if you’re just calling localStorage or don’t want caching, useEffect is still fine.
🧠 Final Thoughts
React gave us the tools. TanStack Query gives us the power.
By replacing manual useEffect data fetching with TanStack Query, you're writing cleaner, faster, and more scalable React apps. It’s a mindset shift — from imperative to declarative data fetching.
Don’t just fetch data. Query it intelligently.
Top comments (0)