Mastering TanStack Query: From Basics to Advanced Patterns
Complete guide to TanStack Query (React Query) covering caching strategies, optimistic updates, infinite queries, and real-time data synchronization in React applications.
TanStack Query (formerly React Query) revolutionizes data fetching in React applications by providing powerful caching, background updates, and synchronization features. This comprehensive guide covers everything from basic usage to advanced patterns for building robust data-driven applications.
Getting Started with TanStack Query
# Install TanStack Querynpm install @tanstack/react-query# Install devtools (optional but recommended)npm install @tanstack/react-query-devtools
Set up the QueryClient and provider in your app:
import React from 'react';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 cacheTime: 1000 * 60 * 10, // 10 minutes retry: 3, refetchOnWindowFocus: false, }, },});export function App() { return ( <QueryClientProvider client={queryClient}> <div className="App"> {/* Your app components */} </div> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> );}
Basic Data Fetching with useQuery
import React from 'react';import { useQuery } from '@tanstack/react-query';interface User { id: number; name: string; email: string; avatar: string;}async function fetchUser(userId: number): Promise<User> { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error('Failed to fetch user'); } return response.json();}export function UserProfile({ userId }: { userId: number }) { const { data: user, isLoading, error, isError } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), enabled: !!userId, // Only run if userId exists }); if (isLoading) return <div>Loading user...</div>; if (isError) return <div>Error: {error.message}</div>; if (!user) return <div>User not found</div>; return ( <div className="user-profile"> <img src={user.avatar} alt={user.name} /> <h2>{user.name}</h2> <p>{user.email}</p> </div> );}
Mutations for Data Updates
import React, { useState } from 'react';import { useMutation, useQueryClient } from '@tanstack/react-query';interface UpdateUserData { name: string; email: string;}async function updateUser(userId: number, data: UpdateUserData): Promise<User> { const response = await fetch(`/api/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error('Failed to update user'); } return response.json();}export function UserForm({ userId, initialData }: { userId: number; initialData: UpdateUserData;}) { const [formData, setFormData] = useState(initialData); const queryClient = useQueryClient(); const updateUserMutation = useMutation({ mutationFn: (data: UpdateUserData) => updateUser(userId, data), onSuccess: (updatedUser) => { // Update the cache with new data queryClient.setQueryData(['user', userId], updatedUser); // Invalidate related queries queryClient.invalidateQueries({ queryKey: ['users'] }); console.log('User updated successfully!'); }, onError: (error) => { console.error('Failed to update user:', error); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); updateUserMutation.mutate(formData); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={formData.name} onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="Name" /> <input type="email" value={formData.email} onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))} placeholder="Email" /> <button type="submit" disabled={updateUserMutation.isPending} > {updateUserMutation.isPending ? 'Updating...' : 'Update User'} </button> {updateUserMutation.isError && ( <div className="error"> Error: {updateUserMutation.error.message} </div> )} </form> );}
Optimistic Updates
import React from 'react';import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';interface Todo { id: number; text: string; completed: boolean;}export function TodoList() { const queryClient = useQueryClient(); const { data: todos = [] } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); const toggleTodoMutation = useMutation({ mutationFn: toggleTodo, onMutate: async (todoId: number) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ['todos'] }); // Snapshot the previous value const previousTodos = queryClient.getQueryData<Todo[]>(['todos']); // Optimistically update to the new value queryClient.setQueryData<Todo[]>(['todos'], (old = []) => old.map(todo => todo.id === todoId ? { ...todo, completed: !todo.completed } : todo ) ); // Return a context object with the snapshotted value return { previousTodos }; }, onError: (err, todoId, context) => { // If the mutation fails, rollback to the previous value if (context?.previousTodos) { queryClient.setQueryData(['todos'], context.previousTodos); } }, onSettled: () => { // Always refetch after error or success queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); return ( <ul> {todos.map(todo => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodoMutation.mutate(todo.id)} /> {todo.text} </label> </li> ))} </ul> );}
Infinite Queries for Pagination
import React from 'react';import { useInfiniteQuery } from '@tanstack/react-query';interface UsersResponse { users: User[]; nextCursor?: number; hasMore: boolean;}async function fetchUsers({ pageParam = 0 }): Promise<UsersResponse> { const response = await fetch(`/api/users?cursor=${pageParam}&limit=10`); if (!response.ok) throw new Error('Failed to fetch users'); return response.json();}export function InfiniteUserList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error, } = useInfiniteQuery({ queryKey: ['users', 'infinite'], queryFn: fetchUsers, getNextPageParam: (lastPage) => lastPage.nextCursor, initialPageParam: 0, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.users.map(user => ( <div key={user.id} className="user-item"> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'No more users'} </button> </div> );}
Custom Hooks for Reusability
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';// Custom hook for user dataexport function useUser(userId: number) { return useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), enabled: !!userId, staleTime: 1000 * 60 * 5, // 5 minutes });}// Custom hook for user mutationsexport function useUserMutations(userId: number) { const queryClient = useQueryClient(); const updateUser = useMutation({ mutationFn: (data: UpdateUserData) => updateUserApi(userId, data), onSuccess: (updatedUser) => { queryClient.setQueryData(['user', userId], updatedUser); queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); const deleteUser = useMutation({ mutationFn: () => deleteUserApi(userId), onSuccess: () => { queryClient.removeQueries({ queryKey: ['user', userId] }); queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); return { updateUser, deleteUser, };}// Custom hook with dependent queriesexport function useUserWithPosts(userId: number) { const userQuery = useUser(userId); const postsQuery = useQuery({ queryKey: ['posts', userId], queryFn: () => fetchUserPosts(userId), enabled: !!userQuery.data, // Only fetch posts if user exists }); return { user: userQuery.data, posts: postsQuery.data, isLoading: userQuery.isLoading || postsQuery.isLoading, error: userQuery.error || postsQuery.error, };}
Automatic background updates, cache invalidation, and intelligent data synchronization.
Update UI immediately for better UX, with automatic rollback on errors.
Built-in pagination support for infinite scrolling and load-more patterns.
Powerful debugging tools for inspecting queries, cache, and mutations.