TanStack Query: Server State Management for React
Master TanStack Query (React Query) for data fetching. Learn queries, mutations, caching, pagination, optimistic updates, and real-world patterns.
Moshiour Rahman
Advertisement
What is TanStack Query?
TanStack Query (formerly React Query) is a powerful library for managing server state in React applications. It handles data fetching, caching, synchronization, and updates with minimal configuration.
Why TanStack Query?
| Problem | Solution |
|---|---|
| Manual loading states | Automatic state management |
| Duplicate requests | Request deduplication |
| Stale data | Smart caching and refetching |
| Complex cache invalidation | Simple cache management |
| Optimistic updates | Built-in support |
Getting Started
Installation
npm install @tanstack/react-query
Setup Provider
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true
}
}
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Basic Queries
Simple Query
import { useQuery } from '@tanstack/react-query';
function fetchUsers() {
return fetch('/api/users').then((res) => res.json());
}
function UserList() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Query with Parameters
function fetchUser(userId: string) {
return fetch(`/api/users/${userId}`).then((res) => res.json());
}
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId // Only fetch when userId exists
});
if (isLoading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
Query with Search Params
interface SearchParams {
query: string;
page: number;
limit: number;
}
function searchUsers(params: SearchParams) {
const searchParams = new URLSearchParams({
q: params.query,
page: String(params.page),
limit: String(params.limit)
});
return fetch(`/api/users/search?${searchParams}`).then((res) => res.json());
}
function UserSearch() {
const [search, setSearch] = useState({ query: '', page: 1, limit: 10 });
const { data, isFetching } = useQuery({
queryKey: ['users', 'search', search],
queryFn: () => searchUsers(search),
placeholderData: (previousData) => previousData, // Keep previous data while fetching
});
return (
<div>
<input
value={search.query}
onChange={(e) => setSearch({ ...search, query: e.target.value, page: 1 })}
placeholder="Search users..."
/>
{isFetching && <span>Searching...</span>}
{/* Results */}
</div>
);
}
Mutations
Basic Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
function createUser(userData: { name: string; email: string }) {
return fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
}).then((res) => res.json());
}
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error('Error creating user:', error);
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ name: 'John', email: 'john@example.com' });
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>User created!</p>}
</form>
);
}
Mutation with Variables
interface UpdateUserData {
id: string;
name?: string;
email?: string;
}
function updateUser({ id, ...data }: UpdateUserData) {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then((res) => res.json());
}
function UserEditForm({ user }: { user: User }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data) => {
// Update specific query
queryClient.setQueryData(['user', user.id], data);
// Invalidate list
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
return (
<button onClick={() => mutation.mutate({ id: user.id, name: 'Updated Name' })}>
Update
</button>
);
}
Optimistic Updates
function useTodoMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.map((todo) =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
)
);
// Return context with snapshot
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
}
Pagination
Basic Pagination
function fetchProjects(page: number) {
return fetch(`/api/projects?page=${page}&limit=10`).then((res) => res.json());
}
function ProjectList() {
const [page, setPage] = useState(1);
const { data, isLoading, isFetching, isPlaceholderData } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
placeholderData: (previousData) => previousData
});
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : (
<>
<ul style={{ opacity: isFetching ? 0.5 : 1 }}>
{data.projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
<div>
<button
onClick={() => setPage((p) => Math.max(p - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
Next
</button>
</div>
</>
)}
</div>
);
}
Infinite Scroll
import { useInfiniteQuery } from '@tanstack/react-query';
function fetchPosts({ pageParam = 1 }) {
return fetch(`/api/posts?page=${pageParam}&limit=20`).then((res) => res.json());
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
}
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data.pages.map((page, i) => (
<React.Fragment key={i}>
{page.posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'No more posts'}
</button>
</div>
);
}
Advanced Patterns
Dependent Queries
function UserPosts({ userId }: { userId: string }) {
// First query - get user
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// Second query - depends on first
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user // Only runs when user is available
});
return (
<div>
<h2>{user?.name}'s Posts</h2>
{posts?.map((post) => <PostCard key={post.id} post={post} />)}
</div>
);
}
Parallel Queries
import { useQueries } from '@tanstack/react-query';
function Dashboard({ userIds }: { userIds: string[] }) {
const results = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id)
}))
});
const isLoading = results.some((r) => r.isLoading);
const users = results.map((r) => r.data).filter(Boolean);
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
Prefetching
function UserList() {
const queryClient = useQueryClient();
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
// Prefetch on hover
const prefetchUser = (userId: string) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5
});
};
return (
<ul>
{users?.map((user) => (
<li
key={user.id}
onMouseEnter={() => prefetchUser(user.id)}
>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
Initial Data
// From cache
function UserProfile({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
initialData: () => {
// Get from users list cache
const users = queryClient.getQueryData(['users']);
return users?.find((u) => u.id === userId);
},
initialDataUpdatedAt: () => {
// When was parent query updated?
return queryClient.getQueryState(['users'])?.dataUpdatedAt;
}
});
}
// From props
function UserDetail({ user }: { user: User }) {
const { data } = useQuery({
queryKey: ['user', user.id],
queryFn: () => fetchUser(user.id),
initialData: user
});
}
Cache Management
Manual Cache Updates
const queryClient = useQueryClient();
// Get cached data
const users = queryClient.getQueryData(['users']);
// Set cached data
queryClient.setQueryData(['users'], (old) => [...old, newUser]);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['users'] });
// Remove from cache
queryClient.removeQueries({ queryKey: ['users'] });
// Reset to initial state
queryClient.resetQueries({ queryKey: ['users'] });
// Cancel ongoing queries
queryClient.cancelQueries({ queryKey: ['users'] });
Query Filters
// Invalidate all user-related queries
queryClient.invalidateQueries({ queryKey: ['users'] });
// Invalidate exact query
queryClient.invalidateQueries({
queryKey: ['users', 'list'],
exact: true
});
// Invalidate by predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'users' && query.state.data?.length > 0
});
Custom Hooks
useUser Hook
function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5
});
}
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onSuccess: (data) => {
queryClient.setQueryData(['user', data.id], data);
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}
// Usage
function Profile({ userId }) {
const { data: user, isLoading } = useUser(userId);
const { mutate: updateUser, isPending } = useUpdateUser();
// ...
}
Summary
| Feature | Hook |
|---|---|
| Fetch data | useQuery |
| Modify data | useMutation |
| Multiple queries | useQueries |
| Infinite scroll | useInfiniteQuery |
| Access client | useQueryClient |
| Suspense | useSuspenseQuery |
TanStack Query simplifies data fetching with automatic caching, background updates, and powerful developer tools.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
React Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptZustand: Lightweight React State Management Made Simple
Master Zustand for React state management. Learn store creation, async actions, middleware, persistence, and TypeScript integration patterns.
JavaScriptVite + React: Modern Frontend Development Setup
Master Vite for React development. Learn project setup, configuration, plugins, environment variables, and production optimization.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.