What is TanStack Query?
TanStack Query (formerly known as React Query) is a powerful data-fetching and state management library for JavaScript applications. It simplifies the process of fetching, caching, synchronizing, and updating server state in your web apps. While it was originally built for React, TanStack Query now supports multiple frameworks including Vue, Solid, Svelte, and even plain JavaScript.
At its core, TanStack Query treats server state as a first-class citizen. It provides hooks and utilities to manage asynchronous data with minimal boilerplate, automatic caching, background refetching, pagination, optimistic updates, and much more.
Why TanStack Query Matters
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Traditional approaches to data fetching in modern web apps often lead to scattered logic, manual caching, race conditions, and repetitive code. TanStack Query addresses these pain points by offering:
- Declarative Data Fetching: Define your queries and mutations declaratively, and let the library handle loading, error, and success states.
- Automatic Caching & Deduplication: Requests are automatically cached and deduplicated, so multiple components can share the same data without redundant network calls.
- Background Refetching: Keep data fresh by automatically refetching when the window regains focus, the network reconnects, or a specified interval elapses.
- Optimistic Updates: Update the UI optimistically before the server confirms a mutation, providing a snappy user experience.
- Pagination & Infinite Scrolling: Built-in support for cursor-based pagination and infinite queries.
- Devtools: A dedicated devtool panel to inspect queries, cache state, and mutation history.
Getting Started: Installation and Setup
To begin using TanStack Query in a React project, install the core library and the React adapter:
npm install @tanstack/react-query
Next, wrap your application with the QueryClientProvider and create a QueryClient instance:
// App.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
Basic Query Example
Fetching data with TanStack Query is done via the useQuery hook. Here's a simple example that fetches a list of posts from an API:
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
function fetchPosts() {
return axios.get('/api/posts').then(res => res.data);
}
function PostsList() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Key points:
queryKeyuniquely identifies the query and is used for caching, refetching, and sharing.queryFnis the asynchronous function that returns the data.- The hook returns
data,isLoading,isError, anderrorfor easy state management.
Query Configuration and Options
TanStack Query offers extensive configuration options via the third argument to useQuery or through the defaultOptions on the QueryClient. Common options include:
staleTime: How long data remains "fresh" before being considered stale (in milliseconds). Default is 0, meaning data is immediately stale.cacheTime: How long inactive data stays in memory before garbage collection.refetchInterval: Automatically refetch at a given interval (e.g., for polling).enabled: Conditionally run the query (e.g., only if a user ID exists).
useQuery({
queryKey: ['posts', { page }],
queryFn: () => fetchPosts(page),
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
refetchInterval: false,
enabled: !!page,
});
Mutations: Creating, Updating, and Deleting Data
For modifying server data, use the useMutation hook. It handles the mutation lifecycle (loading, error, success) and integrates with the cache for invalidation.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function addPost(newPost) {
return axios.post('/api/posts', newPost).then(res => res.data);
}
function NewPostForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addPost,
onSuccess: () => {
// Invalidate and refetch the 'posts' query
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
mutation.mutate({
title: formData.get('title'),
body: formData.get('body'),
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Body" required />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Adding...' : 'Add Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post added!</p>}
</form>
);
}
Optimistic Updates
Optimistic updates allow you to update the UI before the server responds, making the app feel instant. Use onMutate to modify the cache, and onSettled to either rollback on error or invalidate on success.
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel any outgoing refetches to avoid overwriting optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value for rollback
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update the cache
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => (todo.id === newTodo.id ? { ...todo, ...newTodo } : todo))
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback to the previous state
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Always refetch after error or success to ensure server state
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Pagination and Infinite Queries
TanStack Query provides useInfiniteQuery for handling paginated data with "load more" or infinite scroll patterns.
import { useInfiniteQuery } from '@tanstack/react-query';
function fetchProjects({ pageParam = 0 }) {
return axios.get('/api/projects', { params: { offset: pageParam, limit: 10 } });
}
function ProjectsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, allPages) => {
// Return the offset for the next page, or undefined if no more pages
return lastPage.nextOffset ?? undefined;
},
});
if (status === 'loading') return <div>Loading...</div>;
if (status === 'error') return <div>Error</div>;
return (
<div>
{data.pages.map((group, i) => (
<div key={i}>
{group.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</div>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
</div>
);
}
Query Invalidation and Refetching
To force a refetch of specific queries after a mutation or user action, use queryClient.invalidateQueries or refetchQueries.
// Invalidate all queries with key starting with 'posts'
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Refetch a single query
queryClient.refetchQueries({ queryKey: ['user', userId] });
Devtools
TanStack Query ships with a powerful devtools panel. Install the devtools package:
npm install @tanstack/react-query-devtools
Then add it to your app:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Best Practices
- Use meaningful query keys: Structure keys as arrays (e.g.,
['todos', { status: 'done' }]) to enable granular invalidation and caching. - Separate query functions: Define your API functions (like
fetchPosts) in a dedicatedapimodule to keep hooks clean. - Set sensible stale times: Avoid unnecessary network requests by setting
staleTimeappropriate for your dataβs freshness needs. - Leverage
enabled: Disable queries that depend on user input or other conditions to avoid fetching incomplete data. - Handle errors globally: Use the
QueryClientdefault options or a customonErrorto log or toast errors consistently. - Use optimistic updates sparingly: Only optimistic for actions that are highly likely to succeed (e.g., toggling a favorite) and always provide a rollback.
- Keep mutations lean: Mutations should only handle the mutation itself; use
onSuccessto invalidate related queries rather than manually updating state. - Combine with state management: TanStack Query manages server state; for client-only state (e.g., UI toggles, form inputs), continue using
useState,useReducer, or a tool like Zustand.
Conclusion
TanStack Query revolutionizes the way developers handle server state in modern web applications. By abstracting away caching, synchronization, and loading states, it lets you focus on building features rather than boilerplate. Its robust API supports everything from simple data fetching to complex pagination and optimistic updates, making it an essential tool for any serious web developer. Start small, adopt its patterns gradually, and you'll soon wonder how you ever managed data without it.