Loading...
Loading...
TanStack Query (React Query) v5 best practices for data fetching, caching, mutations, and server state management. Use when building data-driven React applications, setting up query configurations, implementing mutations/optimistic updates, configuring caching strategies, integrating with SSR, or fixing v4→v5 migration errors.
npx skill4agent add fellipeutaka/leon tanstack-queryvariablesuseErrorBoundaryonlinealwaysofflineFirstqueryOptionsonErroronSuccessonSettledonMutateResultnpm install @tanstack/react-query@latest @tanstack/react-query-devtools@latestimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 min
gcTime: 1000 * 60 * 60, // 1 hour
refetchOnWindowFocus: false,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}TanStackDevtoolsnpm install -D @tanstack/react-devtoolsimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[
{ name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
// Add more plugins: Router, etc.
]}
/>
</QueryClientProvider>
)
}*PanelReactQueryDevtoolsPanelTanStackRouterDevtoolsPanelTanStackDevtoolsimport { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
function useTodos() {
return useQuery(todosQueryOptions)
}
function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: { title: string }) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}| Priority | Category | Rule File | Impact |
|---|---|---|---|
| CRITICAL | Query Keys | | Prevents cache bugs and data inconsistencies |
| CRITICAL | Caching | | Optimizes performance and data freshness |
| HIGH | Invalidation | | Ensures stale data is properly refreshed |
| HIGH | Mutations | | Ensures data integrity after writes |
| HIGH | Optimistic Updates | | Responsive UI during mutations |
| HIGH | Error Handling | | Prevents poor user experiences |
| MEDIUM | Prefetching | | Improves perceived performance |
| MEDIUM | Infinite Queries | | Prevents pagination bugs |
| MEDIUM | SSR/Hydration | | Enables proper server rendering |
| MEDIUM | Parallel Queries | | Dynamic parallel fetching |
| LOW | Performance | | Reduces unnecessary re-renders |
| LOW | Offline Support | | Enables offline-first patterns |
useQuery({ queryKey, queryFn, ...options })['todos']['todos', id]['todos', { filter }]if (!res.ok) throw new Error('Failed')if (isPending) return <Loading />onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })useQueryuseSuspenseQueryprefetchQueryuseQuery(['todos'], fetchTodos)onSuccessonErroronSettleduseEffectisPendingplaceholderData: keepPreviousData| v4 | v5 | Notes |
|---|---|---|
| | Object syntax only |
| | Renamed |
| | |
| | Import |
| | Renamed |
| Removed | Use |
| | Required for infinite queries |
| | Renamed |
| | v5.89+ added 4th param |
void prefetchQueryuseSuspenseQueryisFetchingawaitfetchStatususeQueryisLoadinguseSuspenseQueryretryOnMount: falserefetchOnMount: falsemutation.state.variablesunknownselectrefetchType: 'all'// Dependent queries (B waits for A)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user,
})
// Parallel queries
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})
// Prefetch on hover
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })
// Infinite scroll
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Query cancellation
queryFn: async ({ signal }) => {
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
}
// Data transformation
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })