Skip to main content
tanstack-query Use when managing server state with TanStack Query v5. Covers query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and best practices for async state management.
Zur Installation springen Skills Marktplatz Entdecken und erkunden Sie KI-Skills, die von der Community erstellt wurden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Prompt kopierenPrompt-Details anzeigen Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
npx skills add https://github.com/MadAppGang/claude-code --skill tanstack-queryDer Befehl bleibt in einer Zeile. Scrollen Sie horizontal, um ihn vor dem Kopieren vollständig zu prüfen.
Sie bevorzugen eine lokale Kopie? Laden Sie die Dateien herunter, die SkillsMP derzeit vorliegen.
ZIP herunterladen Herunterladen... Mehr aus diesem Repository Verwandte Berufe
SOC
Basierend auf der SOC-Berufsklassifikation
name tanstack-query description Use when managing server state with TanStack Query v5. Covers query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and best practices for async state management. updated "2026-01-20T00:00:00.000Z" keywords tanstack-query, react-query, server-state, data-fetching, cache, mutations, msw
TanStack Query v5 - Complete Guide
TanStack Query v5 (October 2023) is the async state manager for this project. It requires React 18+, features first-class Suspense support, improved TypeScript inference, and a 20% smaller bundle. This section covers production-ready patterns based on official documentation and community best practices.
Breaking Changes in v5
Key updates you need to know:
Single Object Signature : All hooks now accept one configuration object:
useQuery ({ queryKey, queryFn, ...options })
useQuery (queryKey, queryFn, options)
Renamed Options :
cacheTime → gcTime (garbage collection time)
→
keepPreviousData
placeholderData: keepPreviousData
isLoading now means isPending && isFetching
Callbacks Removed from useQuery :
onSuccess, onError, onSettled removed from useQuery
Use global QueryCache callbacks instead
Prevents duplicate executions
Infinite Queries Require initialPageParam :
No default value provided
Must explicitly set initialPageParam (e.g., 0 or null)
First-Class Suspense :
New dedicated hooks: useSuspenseQuery, useSuspenseInfiniteQuery
No experimental flag needed
Data is never undefined at type level
Migration : Use the official codemod for automatic migration: npx @tanstack/query-codemods v5/replace-import-specifier
Smart Defaults Query v5 ships with production-ready defaults:
{
staleTime : 0 ,
gcTime : 5 * 60_000 ,
retry : 3 ,
refetchOnWindowFocus : true ,
refetchOnReconnect : true ,
}
Philosophy : React Query is an async state manager, not a data fetcher . You provide the Promise; Query manages caching, background updates, and synchronization.
Client Setup
import { QueryClient , QueryClientProvider , QueryCache } from '@tanstack/react-query'
import { toast } from './toast'
const queryClient = new QueryClient ({
defaultOptions : {
queries : {
staleTime : 0 ,
gcTime : 5 * 60_000 ,
retry : (failureCount, error ) => {
if (error?.response ?.status === 401 ) return false
return failureCount < 3
},
},
},
queryCache : new QueryCache ({
onError : (error, query ) => {
if (query.state .data !== undefined ) {
toast.error (`Something went wrong: ${error.message} ` )
}
},
}),
})
export function AppProviders ({ children }: { children: React.ReactNode } ) {
return (
<QueryClientProvider client ={queryClient} >
{children}
</QueryClientProvider >
)
}
DevTools Setup (auto-excluded in production):
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false } />
</QueryClientProvider >
Architecture: Feature-Based Colocation Recommended pattern : Group queries with related features, not by file type.
src/features/
├── Todos/
│ ├── index.tsx # Feature entry point
│ ├── queries.ts # All React Query logic (keys, functions, hooks)
│ ├── types.ts # TypeScript types
│ └── components/ # Feature-specific components
Export only custom hooks from query files. Keep query functions and keys private:
const todoKeys = {
all : ['todos' ] as const ,
lists : () => [...todoKeys.all , 'list' ] as const ,
list : (filters : string ) => [...todoKeys.lists (), { filters }] as const ,
details : () => [...todoKeys.all , 'detail' ] as const ,
detail : (id : number ) => [...todoKeys.details (), id] as const ,
}
const fetchTodos = async (filters : string ): Promise <Todo []> => {
const response = await axios.get ('/api/todos' , { params : { filters } })
return response.data
}
export const useTodosQuery = (filters : string ) => {
return useQuery ({
queryKey : todoKeys.list (filters),
queryFn : () => fetchTodos (filters),
staleTime : 30_000 ,
})
}
Prevents key/function mismatches
Clean public API
Encapsulation and maintainability
Easy to locate all query logic for a feature
Query Key Factories (Essential) Structure keys hierarchically from generic to specific:
['todos' ]
['todos' , 'list' ]
['todos' , 'list' , { filters }]
['todos' , 'detail' , 1 ]
['todos-list-active' ]
Critical rule : Query keys must include ALL variables used in queryFn . Treat query keys like dependency arrays:
const { data } = useQuery ({
queryKey : ['todos' , filters, sortBy],
queryFn : () => fetchTodos (filters, sortBy),
})
const { data } = useQuery ({
queryKey : ['todos' ],
queryFn : () => fetchTodos (filters, sortBy),
})
Type consistency matters : ['todos', '1'] and ['todos', 1] are different keys . Be consistent with types.
Query Options API (Type Safety) The modern pattern for maximum type safety across your codebase:
import { queryOptions } from '@tanstack/react-query'
function todoOptions (id : number ) {
return queryOptions ({
queryKey : ['todos' , id],
queryFn : () => fetchTodo (id),
staleTime : 5000 ,
})
}
useQuery (todoOptions (1 ))
queryClient.prefetchQuery (todoOptions (5 ))
queryClient.setQueryData (todoOptions (42 ).queryKey , newTodo)
queryClient.getQueryData (todoOptions (42 ).queryKey )
Single source of truth for query configuration
Full TypeScript inference for imperatively accessed data
Reusable across hooks and imperative methods
Prevents key/function mismatches
Data Transformation Strategies Choose the right approach based on your use case:
1. Transform in queryFn - Simple cases where cache should store transformed data:
const fetchTodos = async (): Promise <Todo []> => {
const response = await axios.get ('/api/todos' )
return response.data .map (todo => ({
...todo,
name : todo.name .toUpperCase ()
}))
}
2. Transform with select option (RECOMMENDED) - Enables partial subscriptions:
export const useTodosQuery = (filters : string ) =>
useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
select : (data ) => data.filter (todo => todo.status === filters),
})
export const useTodosCount = ( ) =>
useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
select : (data ) => data.length ,
})
⚠️ Memoize select functions to prevent running on every render:
const transformTodos = (data : Todo [] ) => expensiveTransform (data)
const query = useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
select : transformTodos,
})
const query = useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
select : (data ) => expensiveTransform (data),
})
TypeScript Best Practices Let TypeScript infer types from queryFn rather than specifying generics:
const { data } = useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
})
const { data } = useQuery<Todo []>({
queryKey : ['todos' ],
queryFn : fetchTodos,
})
Discriminated unions automatically narrow types:
const { data, isSuccess, isError, error } = useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
})
if (isSuccess) {
}
if (isError) {
}
Use queryOptions helper for maximum type safety across imperative methods.
Custom Hooks Pattern Always create custom hooks even for single queries:
export function usePost (
id : number ,
options ?: Omit <UseQueryOptions <Post >, 'queryKey' | 'queryFn' >
) {
return useQuery ({
queryKey : ['posts' , id],
queryFn : () => getPost (id),
...options,
})
}
const { data } = usePost (42 , { staleTime : 10_000 })
Centralizes query logic
Easy to update all usages
Consistent configuration
Better testing
Error Handling (Multi-Layer Strategy) Layer 1: Component-Level - Specific user feedback:
function TaskList ( ) {
const { data, error, isError, isLoading } = useQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
})
if (isLoading) return <Spinner />
if (isError) return <ErrorAlert > {error.message}</ErrorAlert >
return <ul > {data.map(todo => <TodoItem key ={todo.id} {...todo } /> )}</ul >
}
Layer 2: Global Error Handling - Background errors via QueryCache:
queryCache : new QueryCache ({
onError : (error, query ) => {
if (query.state .data !== undefined ) {
toast.error (`Background error: ${error.message} ` )
}
},
})
Layer 3: Error Boundaries - Catch render errors:
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
<QueryErrorResetBoundary >
{({ reset } ) => (
<ErrorBoundary
onReset ={reset}
fallbackRender ={({ error , resetErrorBoundary }) => (
<div >
<p > Error: {error.message}</p >
<button onClick ={resetErrorBoundary} > Try again</button >
</div >
)}
>
<TaskList />
</ErrorBoundary >
)}
</QueryErrorResetBoundary >
Suspense Integration First-class Suspense support in v5 with dedicated hooks:
import { useSuspenseQuery } from '@tanstack/react-query'
function TaskList ( ) {
const { data } = useSuspenseQuery ({
queryKey : ['todos' ],
queryFn : fetchTodos,
})
return <ul > {data.map(todo => <TodoItem key ={todo.id} {...todo } /> )}</ul >
}
function App ( ) {
return (
<Suspense fallback ={ <Spinner /> }>
<TaskList />
</Suspense >
)
}
Eliminates loading state management
Data always defined (TypeScript enforced)
Cleaner component code
Works with React.lazy for code-splitting
Mutations with Optimistic Updates Basic mutation with cache invalidation:
export function useCreateTodo ( ) {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : (newTodo : CreateTodoDTO ) =>
api.post ('/todos' , newTodo).then (res => res.data ),
onSuccess : (data ) => {
queryClient.setQueryData (['todos' , data.id ], data)
queryClient.invalidateQueries ({ queryKey : ['todos' , 'list' ] })
},
})
}
Simple optimistic updates using variables:
const addTodoMutation = useMutation ({
mutationFn : (newTodo : string ) => axios.post ('/api/todos' , { text : newTodo }),
onSettled : () => queryClient.invalidateQueries ({ queryKey : ['todos' ] }),
})
const { isPending, variables, mutate } = addTodoMutation
return (
<ul >
{todoQuery.data?.map(todo => <li key ={todo.id} > {todo.text}</li > )}
{isPending && <li style ={{ opacity: 0.5 }}> {variables}</li > }
</ul >
)
Advanced optimistic updates with rollback:
useMutation ({
mutationFn : updateTodo,
onMutate : async (newTodo) => {
await queryClient.cancelQueries ({ queryKey : ['todos' ] })
const previousTodos = queryClient.getQueryData (['todos' ])
queryClient.setQueryData (['todos' ], (old : Todo [] ) =>
old?.map (todo => todo.id === newTodo.id ? newTodo : todo)
)
return { previousTodos }
},
onError : (err, newTodo, context ) => {
queryClient.setQueryData (['todos' ], context?.previousTodos )
toast.error ('Update failed. Changes reverted.' )
},
onSettled : () => {
queryClient.invalidateQueries ({ queryKey : ['todos' ] })
},
})
Cancel ongoing queries in onMutate to prevent race conditions
Snapshot previous data before updating
Restore snapshot on error
Always invalidate in onSettled for eventual consistency
Never mutate cached data directly - always use immutable updates
Authentication Integration Handle token refresh at HTTP client level (not React Query):
import axios from 'axios'
import createAuthRefreshInterceptor from 'axios-auth-refresh'
export const apiClient = axios.create ({
baseURL : import .meta .env .VITE_API_URL ,
})
apiClient.interceptors .request .use ((config ) => {
const token = getAccessToken ()
if (token) config.headers .Authorization = `Bearer ${token} `
return config
})
const refreshAuth = async (failedRequest : any ) => {
try {
const newToken = await fetchNewToken ()
failedRequest.response .config .headers .Authorization = `Bearer ${newToken} `
setAccessToken (newToken)
return Promise .resolve ()
} catch {
removeAccessToken ()
window .location .href = '/login'
return Promise .reject ()
}
}
createAuthRefreshInterceptor (apiClient, refreshAuth, {
statusCodes : [401 ],
pauseInstanceWhileRefreshing : true ,
})
Protected queries use the enabled option:
const useTodos = ( ) => {
const { user } = useUser ()
return useQuery ({
queryKey : ['todos' , user?.id ],
queryFn : () => fetchTodos (user.id ),
enabled : !!user,
})
}
On logout : Clear the entire cache with queryClient.clear() (not invalidateQueries() which triggers refetches):
const logout = ( ) => {
removeAccessToken ()
queryClient.clear ()
navigate ('/login' )
}
Advanced Patterns Prefetching - Eliminate loading states:
function ShowDetailsButton ( ) {
const queryClient = useQueryClient ()
const prefetch = ( ) => {
queryClient.prefetchQuery ({
queryKey : ['details' ],
queryFn : getDetailsData,
staleTime : 60_000 ,
})
}
return (
<button onMouseEnter ={prefetch} onClick ={showDetails} >
Show Details
</button >
)
}
Infinite Queries - Infinite scrolling/pagination:
function Projects ( ) {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery ({
queryKey : ['projects' ],
queryFn : ({ pageParam } ) => fetchProjects (pageParam),
initialPageParam : 0 ,
getNextPageParam : (lastPage ) => lastPage.nextCursor ,
})
if (isLoading) return <Spinner />
return (
<>
{data.pages.map((page, i) => (
<React.Fragment key ={i} >
{page.data.map(project => (
<ProjectCard key ={project.id} {...project } />
))}
</React.Fragment >
))}
<button
onClick ={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button >
</>
)
}
Offset-Based Pagination with placeholderData:
import { keepPreviousData } from '@tanstack/react-query'
function Posts ( ) {
const [page, setPage] = useState (0 )
const { data, isPending, isPlaceholderData } = useQuery ({
queryKey : ['posts' , page],
queryFn : () => fetchPosts (page),
placeholderData : keepPreviousData,
})
return (
<>
{data.posts.map(post => <PostCard key ={post.id} {...post } /> )}
<button
onClick ={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0}
>
Previous
</button >
<button
onClick ={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
Next
</button >
</>
)
}
Dependent Queries - Sequential data fetching:
function UserProjects ({ email }: { email: string } ) {
const { data : user } = useQuery ({
queryKey : ['user' , email],
queryFn : () => getUserByEmail (email),
})
const { data : projects } = useQuery ({
queryKey : ['projects' , user?.id ],
queryFn : () => getProjectsByUser (user.id ),
enabled : !!user?.id ,
})
return <div > {/* render projects */}</div >
}
Performance Optimization staleTime is your primary control - adjust this, not gcTime:
staleTime : 0
staleTime : 1000 * 60 * 2
staleTime : 1000 * 60 * 10
Query deduplication happens automatically - multiple components mounting with identical query keys result in a single network request, but all components receive data.
Prevent request waterfalls :
function Dashboard ( ) {
const { data : user } = useQuery (userQuery)
const { data : posts } = useQuery (postsQuery (user?.id ))
const { data : stats } = useQuery (statsQuery (user?.id ))
}
function Dashboard ( ) {
const { data : user } = useQuery (userQuery)
const { data : posts } = useQuery ({
...postsQuery (user?.id ),
enabled : !!user?.id ,
})
const { data : stats } = useQuery ({
...statsQuery (user?.id ),
enabled : !!user?.id ,
})
}
Never copy server state to local state - this opts out of background updates:
const { data } = useQuery ({ queryKey : ['todos' ], queryFn : fetchTodos })
const [todos, setTodos] = useState (data)
const { data : todos } = useQuery ({ queryKey : ['todos' ], queryFn : fetchTodos })
Testing with Mock Service Worker (MSW) MSW is the recommended approach - mock the network layer:
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get ('/api/todos' , () => {
return HttpResponse .json ([
{ id : 1 , text : 'Test todo' , completed : false },
])
}),
http.post ('/api/todos' , async ({ request }) => {
const newTodo = await request.json ()
return HttpResponse .json ({ id : 2 , ...newTodo })
}),
]
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'
export const server = setupServer (...handlers)
beforeAll (() => server.listen ())
afterEach (() => server.resetHandlers ())
afterAll (() => server.close ())
Create test wrappers with proper QueryClient:
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
export function createTestQueryClient ( ) {
return new QueryClient ({
defaultOptions : {
queries : {
retry : false ,
gcTime : Infinity ,
},
},
})
}
export function renderWithClient (ui : React .ReactElement ) {
const testQueryClient = createTestQueryClient ()
return render (
<QueryClientProvider client ={testQueryClient} >
{ui}
</QueryClientProvider >
)
}
import { renderWithClient } from '@/test/utils'
import { screen } from '@testing-library/react'
test ('displays todos' , async () => {
renderWithClient (<TaskList /> )
expect (await screen.findByText ('Test todo' )).toBeInTheDocument ()
})
test ('shows error state' , async () => {
server.use (
http.get ('/api/todos' , () => {
return HttpResponse .json (
{ message : 'Failed to fetch' },
{ status : 500 }
)
})
)
renderWithClient (<TaskList /> )
expect (await screen.findByText (/failed/i )).toBeInTheDocument ()
})
Critical testing principles :
Create new QueryClient per test for isolation
Set retry: false to prevent timeouts
Use async queries (findBy*) for data that loads
Silence console.error for expected errors
Anti-Patterns to Avoid ❌ Don't store query data in Redux/Context :
Creates dual sources of truth
Loses automatic cache invalidation
Triggers unnecessary renders
❌ Don't call refetch() with different parameters :
const { data, refetch } = useQuery ({
queryKey : ['todos' ],
queryFn : () => fetchTodos (filters),
})
const [filters, setFilters] = useState ('all' )
const { data } = useQuery ({
queryKey : ['todos' , filters],
queryFn : () => fetchTodos (filters),
})
❌ Don't use queries for local state :
Query Cache expects refetchable data
Use useState/useReducer for client-only state
❌ Don't create QueryClient inside components :
function App ( ) {
const client = new QueryClient ()
return <QueryClientProvider client ={client} > ...</QueryClientProvider >
}
const queryClient = new QueryClient ()
function App ( ) {
return <QueryClientProvider client ={queryClient} > ...</QueryClientProvider >
}
❌ Don't ignore loading and error states - always handle both
❌ Don't transform data by copying to state - use select option
❌ Don't mismatch query keys - be consistent with types ('1' vs 1)
Cache Timing Guidelines staleTime - How long data is considered fresh:
0 (default) - Always stale, refetch on mount/focus
30_000 (30s) - Good for user-generated content
120_000 (2min) - Good for profile data
600_000 (10min) - Good for static reference data
gcTime (formerly cacheTime) - How long unused data stays in cache:
300_000 (5min, default) - Good for most cases
Infinity - Keep forever (useful with persistence)
0 - Immediate garbage collection (not recommended)
Relationship : staleTime controls refetch frequency, gcTime controls memory cleanup.
Related Skills
router-query-integration - Integrating Query with TanStack Router loaders
api-integration - Apidog + OpenAPI integration
react-patterns - Choose between Query mutations vs React Actions
testing-strategy - Advanced MSW patterns