TanStack Query
TanStack Query cheat sheet with useQuery, useMutation, caching strategies, optimistic updates, and server state management examples.
Other React Sheets
Setup & Configuration
Installing and configuring TanStack Query
Setting up TanStack Query in your React application
# Installation
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
// App.tsx - Setup QueryClient & Provider
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}Queries
Fetching and caching server data
Using useQuery for data fetching
import { useQuery } from '@tanstack/react-query'
// Basic query
function Profile() {
const { data, error, isLoading } = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile
})
if (isLoading) return 'Loading...'
if (error) return 'An error occurred: ' + error.message
return <div>{data.name}</div>
}
// Query with parameters
function User({ userId }) {
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId // Only run if userId exists
})
return <div>{data?.name}</div>
}Queries that depend on other queries
// Sequential dependent queries
function UserProjects({ userId }) {
// First query
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
// Second query depends on first
const projectsQuery = useQuery({
queryKey: ['projects', userQuery.data?.id],
queryFn: () => fetchProjects(userQuery.data.id),
enabled: !!userQuery.data?.id // Only run when user data is available
})
return (
<div>
<h1>{userQuery.data?.name}</h1>
<ul>
{projectsQuery.data?.map(project => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</div>
)
}Running multiple queries simultaneously
import { useQueries } from '@tanstack/react-query'
// Multiple parallel queries with useQueries
function Dashboard({ userIds }) {
const userQueries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 1000 * 60 * 5
}))
})
const isLoading = userQueries.some(query => query.isLoading)
const isError = userQueries.some(query => query.isError)
if (isLoading) return 'Loading users...'
if (isError) return 'Error loading users'
return (
<div>
{userQueries.map((query, index) => (
<div key={userIds[index]}>
{query.data?.name}
</div>
))}
</div>
)
}Mutations
Creating, updating, and deleting data
Using useMutation for data modifications
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreatePost() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
mutation.mutate({
title: formData.get('title'),
content: formData.get('content')
})
}}
>
<input name="title" />
<textarea name="content" />
<button type="submit">
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && (
<div>Error: {mutation.error.message}</div>
)}
</form>
)
}Implementing update and delete operations
// Update mutation
function EditPost({ post }) {
const queryClient = useQueryClient()
const updateMutation = useMutation({
mutationFn: ({ id, ...data }) => updatePost(id, data),
onSuccess: (data) => {
// Update specific item in cache
queryClient.setQueryData(['post', data.id], data)
// Invalidate list
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
const deleteMutation = useMutation({
mutationFn: deletePost,
onSuccess: (_, postId) => {
// Remove from cache
queryClient.removeQueries({ queryKey: ['post', postId] })
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
return (
<div>
<button onClick={() => updateMutation.mutate({
id: post.id,
title: 'Updated Title'
})}>
Update
</button>
<button onClick={() => deleteMutation.mutate(post.id)}>
Delete
</button>
</div>
)
}Infinite Queries
Implementing pagination and infinite scroll
Loading data progressively with useInfiniteQuery
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
if (isLoading) return 'Loading...'
if (isError) return 'Error!'
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
)
}Cache Management
Managing and manipulating the query cache
Working with the QueryClient cache
import { useQueryClient } from '@tanstack/react-query'
function CacheControls() {
const queryClient = useQueryClient()
// Invalidate queries
const handleInvalidate = () => {
// Invalidate all queries
queryClient.invalidateQueries()
// Invalidate specific queries
queryClient.invalidateQueries({ queryKey: ['posts'] })
// Invalidate with exact match
queryClient.invalidateQueries({
queryKey: ['post', 1],
exact: true
})
}
// Prefetch data
const handlePrefetch = () => {
queryClient.prefetchQuery({
queryKey: ['post', 2],
queryFn: () => fetchPost(2),
staleTime: 1000 * 60 * 5
})
}
// Direct cache manipulation
const handleCacheUpdate = () => {
// Set data directly
queryClient.setQueryData(['user'], { name: 'John' })
// Get cached data
const cachedUser = queryClient.getQueryData(['user'])
// Remove from cache
queryClient.removeQueries({ queryKey: ['old-data'] })
}
return (
<div>
<button onClick={handleInvalidate}>Invalidate</button>
<button onClick={handlePrefetch}>Prefetch</button>
<button onClick={handleCacheUpdate}>Update Cache</button>
</div>
)
}Advanced Patterns
Advanced TanStack Query patterns and techniques
Using React Suspense with TanStack Query
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
// Using suspense query
function Profile({ userId }) {
// This will suspend
const { data } = useSuspenseQuery({
queryKey: ['profile', userId],
queryFn: () => fetchProfile(userId)
})
// Data is guaranteed to be defined
return <div>{data.name}</div>
}
// App with Suspense and Error Boundary
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<Profile userId={1} />
</Suspense>
</ErrorBoundary>
)
}Building reusable query hooks and patterns
// Custom hook for user data
function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5,
})
}
// Custom hook with mutations
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 updateUser = useUpdateUser()
if (isLoading) return 'Loading...'
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}>
Update
</button>
</div>
)
}