Summary
TanStack Query (formerly known as React Query) is a powerful asynchronous state management and data-fetching library for JavaScript applications. It’s part of the broader TanStack ecosystem (which also includes TanStack Table, Router, Form, etc.).
At its core, TanStack Query handles the server-state problem: fetching, caching, synchronizing, and updating remote data in your UI. Instead of manually managing loading states, error states, and cache invalidation yourself, TanStack Query handles all of that for you.
Key Hooks
useQuery- very useful hook for fetching/reading data. You give it a key and a fetch function and it will manage the rest for you (loading, error, stale data, background refetching)useMutation- for creating/updating/deleting data. Gives you callbacks likeonSuccessandonError.
Key Use-Cases
- Query Keys - a unique identifier for each query, used for caching and invalidation. e.g.
['user', userId]. - Stale-While-Revalidate - serves cached data immediately while fetching fresh data in the background.
- Query Invalidation - after a mutation, you can invalidate related queries so they automatically refetch.
Use-Cases
Without it, you’d be writing a lot of useEffect + useState boilerplate for every API call, manually handling race conditions, cache expiry, retry logic, and refetching on window focus. TanStack Query gives you all of that out of the box.
Fetching Data
Plain React
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
fetch('/api/todos')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, []);TanStack Query — useQuery
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
});Fetching with a Dependency (e.g. by ID)
Plain React
useEffect(() => {
if (!userId) return;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, [userId]);TanStack Query — useQuery
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
enabled: !!userId, // won't run until userId exists
});Creating / Updating / Deleting
Plain React
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
try {
await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) });
// manually refetch or update state...
} finally {
setIsPending(false);
}
};TanStack Query — useMutation
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] }); // auto-refetch
},
});
mutation.mutate(newTodo);Multiple Queries in Parallel
Plain React
useEffect(() => {
Promise.all([fetch('/api/users'), fetch('/api/posts')])
.then(([u, p]) => Promise.all([u.json(), p.json()]))
.then(([users, posts]) => { setUsers(users); setPosts(posts); });
}, []);TanStack Query — useQueries
const [usersQuery, postsQuery] = useQueries({
queries: [
{ queryKey: ['users'], queryFn: () => fetch('/api/users').then(r => r.json()) },
{ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()) },
],
});Infinite / Paginated Data
Plain React
const [pages, setPages] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`/api/posts?page=${page}`)
.then(res => res.json())
.then(data => setPages(prev => [...prev, data]));
}, [page]);TanStack Query — useInfiniteQuery
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
});
// data.pages is an array of all fetched pagesAccessing Cached Data Elsewhere
Plain React — you’d need to lift state up, use Context, or a global store (Zustand, Redux, etc.)
TanStack Query — useQueryClient
const queryClient = useQueryClient();
// Read from cache
const todos = queryClient.getQueryData(['todos']);
// Manually set cache
queryClient.setQueryData(['todos'], newTodos);
// Invalidate (trigger refetch)
queryClient.invalidateQueries({ queryKey: ['todos'] });Summary Table
| Use Case | Plain React | TanStack Query |
|---|---|---|
| Fetch data | useEffect + useState | useQuery |
| Fetch with dependency | useEffect with condition | useQuery + enabled |
| Create/update/delete | manual fetch + state | useMutation |
| Parallel queries | Promise.all in useEffect | useQueries |
| Paginated/infinite data | manual page state | useInfiniteQuery |
| Access cache globally | Context / prop drilling | useQueryClient |
The pattern that jumps out most is that TanStack Query essentially replaces most of your useEffect + useState combos for anything server-related, and gives you caching, deduplication, background refetching, and retry logic for free on top of that.
What’s the Industry Standard?
It depends on the stack, but the rough consensus is:
| Context | Standard |
|---|---|
| REST API, modern React app | TanStack Query |
| Next.js/ Vercel ecosystem | SWR (but TanStack Query works fine too) |
| Redux-heavy codebase | RTK Query |
| GraphQL API | Apollo Client |
| Full-stack TypeScript (Next.js, etc.) | tRPC + TanStack Query |
TanStack Query is arguably the most widely adopted for general React + REST setups. It consistently tops npm download charts and is the default recommendation in most modern React tutorials, job postings, and open source projects. SWR is a close second but has fewer features.
Linked Map of Contexts