Every React developer has written useEffect + fetch + useState + loading + error for the hundredth time. TanStack Query replaces all of that with a single hook that handles caching, background refetching, pagination, optimistic updates, and more.
What TanStack Query Gives You for Free
- Smart caching — data is cached and shared between components
- Background refetching — stale data updates automatically
- Pagination/infinite scroll — built-in helpers
- Optimistic updates — instant UI feedback before server confirms
- Retry logic — automatic retries with exponential backoff
- DevTools — visual query inspector
- Framework agnostic — React, Vue, Solid, Svelte, Angular
Quick Start
npm install @tanstack/react-query
// app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
Fetching Data (Replace useEffect Forever)
Before (useEffect):
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch('/api/users')
.then(res => res.json())
.then(data => { if (!cancelled) { setUsers(data); setLoading(false); } })
.catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
return () => { cancelled = true; };
}, []);
// Still missing: caching, refetching, retry, deduplication...
}
After (TanStack Query):
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
// Gets: caching, refetching, retry, deduplication, devtools — FOR FREE
}
Mutations (Create/Update/Delete)
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}).then(res => res.json()),
onSuccess: () => {
// Invalidate and refetch the users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<button onClick={() => mutation.mutate({ name: 'Alice', email: 'a@b.com' })}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
);
}
Infinite Scroll
function Feed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<div>
{data?.pages.map(page =>
page.items.map(post => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
Optimistic Updates
const toggleTodo = useMutation({
mutationFn: (id) => fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' }),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (err, id, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previous);
},
});
The Verdict
TanStack Query turns data fetching from a solved-every-time problem into a solved-once problem. Caching, refetching, pagination, optimistic updates — all handled. Stop writing useEffect for data fetching.
Need help building production web scrapers or data pipelines? I build custom solutions. Reach out: spinov001@gmail.com
Check out my awesome-web-scraping collection — 400+ tools for extracting web data.
Top comments (0)