tanstack-query · bobmatnyc — скилл для Claude Code: что делает и как установить
Учит ИИ-ассистента настраивать TanStack Query в React: работать с серверным состоянием, кешированием, пагинацией и оптимистичными обновлениями.
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a claude-codeЧто делает tanstack-query
Скилл задаёт правила и шаблоны для интеграции библиотеки @tanstack/react-query. Ассистент получает инструкции по настройке QueryClientProvider, хукам useQuery и useMutation, а также по разделению серверных и клиентских данных.
Внутри собраны паттерны для ключей запросов queryKey, управления жизненным циклом через staleTime и gcTime, сброса кеша и фоновой синхронизации. Для состояния форм или UI-флагов скилл прямо предписывает брать Zustand или React Hook Form, оставляя библиотеку строго для удалённых данных.
Какие задачи решает
- настроить QueryClientProvider и ReactQueryDevtools в приложении на React
- организовать иерархическую структуру queryKey для сброса кеша по префиксу
- реализовать мутацию данных с автоматическим обновлением связанных списков
- разграничить работу с серверным состоянием и локальным состоянием интерфейса
Примеры запросов агенту
- Настрой QueryClientProvider с дефолтным staleTime в одну минуту для React-проекта
- Напиши хук useQuery для загрузки профиля пользователя по userId с обработкой ошибок
- Создай форму добавления записи с хуком useMutation и сбросом кеша по ключу users
Как установить tanstack-query
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a claude-code2Вручную: положить файлы
~/.claude/skills/tanstack-query/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/tanstack-query/
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a codex2Вручную: положить файлы
.agents/skills/tanstack-query/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a cursor2Вручную: положить файлы
.cursor/skills/tanstack-query/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install bobmatnyc/claude-mpm-skills tanstack-query2Вручную: положить файлы
.github/skills/tanstack-query/SKILL.md1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a gemini-cli2Командой платформы
gemini skills install https://github.com/bobmatnyc/claude-mpm-skills --path toolchains/typescript/state/tanstack-queryВручную: положить файлы
.devin/skills/tanstack-query/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о tanstack-query
Как установить tanstack-query в Claude Code?
Выполните команду npx skills add bobmatnyc/claude-mpm-skills --skill tanstack-query -a claude-code. Либо добавьте файл по пути ~/.claude/skills/tanstack-query/SKILL.md.
Для каких задач скилл не рекомендует использовать TanStack Query?
Библиотеку не советуют применять для чисто клиентского состояния вроде модальных окон или полей форм. Для этого в руководстве предложены Zustand, Jotai, React Context или React Hook Form.
Какие рекомендации даются по ключам запросов?
Скилл предписывает использовать иерархические массивы вида ['resource', id, 'subresource'] и выносить параметры фильтрации в конец для точной инвалидации кеша.
Исходный текст
SKILL.md · MIT
---
name: tanstack-query
description: TanStack Query (React Query) for asynchronous server-state management with automatic caching, background refetching, optimistic updates, and pagination in React applications.
user-invocable: false
disable-model-invocation: true
progressive_disclosure:
entry_point:
- summary
- when_to_use
- quick_start
intermediate:
- core_concepts
- queries
- mutations
- cache_management
advanced:
- optimistic_updates
- pagination
- ssr_hydration
- integration_patterns
- performance
---
# TanStack Query (React Query) Skill
## Summary
TanStack Query (formerly React Query) is a powerful asynchronous state management library for React that handles server-state fetching, caching, synchronization, and updates. It eliminates the need for manual data fetching boilerplate and provides built-in features like background refetching, optimistic updates, pagination, and intelligent cache management.
## When to Use
**Use TanStack Query when:**
- Fetching data from REST APIs, GraphQL, or tRPC endpoints
- Need automatic background refetching and cache invalidation
- Building real-time dashboards with polling or websocket data
- Implementing infinite scroll or pagination
- Require optimistic UI updates for mutations
- Managing complex server-state synchronization
- Need offline support with cache persistence
- Building applications with frequent data updates
**TanStack Query excels at:**
- Server-state management (API data, external state)
- Request deduplication and caching
- Stale-while-revalidate patterns
- Loading and error state management
- Prefetching and eager loading
- Parallel and dependent query orchestration
**Avoid TanStack Query for:**
- Pure client-side state (use Zustand, Jotai, or Context)
- Form state management (use React Hook Form, Formik)
- Simple one-time fetches without caching needs
## Quick Start
### Installation
```bash
npm install @tanstack/react-query
# DevTools (optional but recommended)
npm install @tanstack/react-query-devtools
```
### Basic Setup
```tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
```
### First Query
```tsx
// components/UserProfile.tsx
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json() as Promise<User>;
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
```
### First Mutation
```tsx
// components/CreateUserForm.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser: { name: string; email: string }) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
</form>
);
}
```
---
## Core Concepts
### Server State vs Client State
**Server State Characteristics:**
- Persisted remotely (database, API, cloud)
- Requires asynchronous APIs for fetching/updating
- Can be out of sync with client
- Can be updated by other users/systems
- Examples: User data, posts, products, settings
**Client State Characteristics:**
- Persisted locally (memory, localStorage)
- Synchronously accessible
- Fully controlled by client
- Examples: UI theme, modal open/closed, form inputs
**TanStack Query manages server state**. Use Zustand/Context for client state.
### Query Keys
Query keys uniquely identify queries and their cached data.
**Key Structure:**
```tsx
// String key (simple)
queryKey: ['todos']
// Array key (recommended for dependencies)
queryKey: ['todo', todoId]
queryKey: ['todos', { status: 'active', page: 1 }]
// Nested arrays (complex hierarchies)
queryKey: ['users', userId, 'posts', { sort: 'date' }]
```
**Key Matching:**
```tsx
// Exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true });
// Prefix match (invalidates all matching)
queryClient.invalidateQueries({ queryKey: ['todos'] }); // Matches ['todos', 1], ['todos', 2], etc.
// Predicate match
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'todos' && query.state.data?.status === 'draft'
});
```
**Best Practices:**
- Use arrays with hierarchical structure: `['resource', id, 'subresource']`
- Place variables at the end: `['users', { filter, sort }]`
- Consistent ordering across components
- Use objects for complex parameters
### Query Lifecycle
```
FRESH → STALE → INACTIVE → GARBAGE COLLECTED
↓ ↓ ↓ ↓
0ms staleTime no observers cacheTime
```
**States:**
- **Fresh**: Data is considered up-to-date (within `staleTime`)
- **Stale**: Data might be outdated, will refetch on trigger
- **Inactive**: No components using the query
- **Garbage Collected**: Removed from cache after `cacheTime`
**Configuration:**
```tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // 5 minutes (data fresh)
gcTime: 10 * 60 * 1000, // 10 minutes (cache retention)
refetchOnWindowFocus: true, // Refetch when window regains focus
refetchOnReconnect: true, // Refetch when reconnecting
refetchInterval: 30000, // Poll every 30 seconds
});
```
### Cache Behavior
**Automatic Caching:**
```tsx
// First component - triggers fetch
function ComponentA() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.name}</div>;
}
// Second component - uses cache instantly
function ComponentB() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.email}</div>; // No second fetch!
}
```
**Stale-While-Revalidate:**
```tsx
// Shows cached data immediately, refetches in background if stale
const { data, isRefetching } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 60000, // Fresh for 1 minute
});
// data available from cache immediately
// isRefetching = true if background refetch happening
```
---
## Queries
### useQuery Hook
**Basic Syntax:**
```tsx
const {
data, // Query result
error, // Error object if failed
isLoading, // First load (no cached data)
isFetching, // Any fetch (including background)
isSuccess, // Query succeeded
isError, // Query failed
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'fetching' | 'paused' | 'idle'
refetch, // Manual refetch function
} = useQuery({
queryKey: ['key'],
queryFn: async () => { /* fetch logic */ },
});
```
### Query Function Patterns
**Basic Fetch:**
```tsx
const { data } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Network error');
return response.json();
},
});
```
**Query Key in Function:**
```tsx
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ queryKey }) => {
const [_key, userId] = queryKey;
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
```
**Abort Signal (Cancellation):**
```tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal });
return response.json();
},
});
// Automatically cancels on unmount or when query becomes inactive
```
**Axios Pattern:**
```tsx
import axios from 'axios';
const { data } = useQuery({
queryKey: ['repos', username],
queryFn: ({ signal }) =>
axios.get(`/api/repos/${username}`, { signal }).then(res => res.data),
});
```
### Dependent Queries
**Sequential Queries:**
```tsx
// Wait for user before fetching projects
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user, // Only run when user exists
});
```
**Conditional Queries:**
```tsx
const { data } = useQuery({
queryKey: ['premium-features', userId],
queryFn: fetchPremiumFeatures,
enabled: user?.isPremium === true, // Only fetch for premium users
});
```
### Parallel Queries
**Manual Parallel:**
```tsx
function Dashboard() {
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const projects = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });
if (users.isLoading || posts.isLoading || projects.isLoading) {
return <Spinner />;
}
return <div>/* render dashboard */</div>;
}
```
**useQueries (Dynamic Parallel):**
```tsx
import { useQueries } from '@tanstack/react-query';
function MultiUserProfiles({ userIds }: { userIds: number[] }) {
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 60000,
})),
});
const allLoaded = results.every(r => r.isSuccess);
if (!allLoaded) return <Spinner />;
return (
<div>
{results.map((result, i) => (
<UserCard key={userIds[i]} user={result.data} />
))}
</div>
);
}
```
### Query Placeholders
**Placeholder Data (Instant UI):**
```tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
placeholderData: [], // Show empty array while loading
});
// Dynamic placeholder from cache
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
placeholderData: () => {
// Use cached list to find placeholder
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
});
```
**Initial Data (Hydration):**
```tsx
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
initialData: () => {
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
initialDataUpdatedAt: () =>
queryClient.getQueryState(['todos'])?.dataUpdatedAt,
});
```
**Difference:**
- `placeholderData`: Not persisted to cache, purely UI
- `initialData`: Persisted to cache as real data
---
## Mutations
### useMutation Hook
**Basic Mutation:**
```tsx
const mutation = useMutation({
mutationFn: async (newTodo: Todo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return response.json();
},
onSuccess: (data) => {
console.log('Created:', data);
},
onError: (error) => {
console.error('Failed:', error);
},
});
// Trigger mutation
mutation.mutate({ title: 'New Todo', done: false });
// Async/await variant
try {
const data = await mutation.mutateAsync(newTodo);
console.log(data);
} catch (error) {
console.error(error);
}
```
**Mutation State:**
```tsx
const {
mutate, // Trigger function
mutateAsync, // Promise variant
data, // Result from successful mutation
error, // Error from failed mutation
isPending, // Mutation in progress
isSuccess, // Mutation succeeded
isError, // Mutation failed
reset, // Reset mutation state
} = useMutation({ /* ... */ });
```
### Cache Invalidation
**Invalidate Queries After Mutation:**
```tsx
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Refetch all 'todos' queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
```
**Multiple Invalidations:**
```tsx
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// Invalidate multiple query families
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['teams', data.teamId] });
},
});
```
**Selective Invalidation:**
```tsx
// Only invalidate specific queries
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // Only ['todos'], not ['todos', 1]
});
// Predicate-based invalidation
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.status === 'draft',
});
```
### Manual Cache Updates
**setQueryData (Direct Update):**
```tsx
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: (updatedTodo) => {
// Update specific todo in cache
queryClient.setQueryData(
['todo', updatedTodo.id],
updatedTodo
);
// Update todo in list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo =>
todo.id === updatedTodo.id ? updatedTodo : todo
)
);
},
});
```
**Immutable Updates:**
```tsx
// Add to list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
[...old, newTodo]
);
// Remove from list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.filter(todo => todo.id !== deletedId)
);
// Update in list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === id ? { ...todo, ...updates } : todo)
);
```
---
## Optimistic Updates
### Basic Optimistic Update
```tsx
const mutation = useMutation({
mutationFn: updateTodo,
// Before mutation executes
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update cache
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
// Return context with snapshot
return { previousTodos };
},
// On error, rollback
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// Always refetch after success or error
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
```
### Complex Optimistic Update Pattern
```tsx
interface Todo {
id: number;
title: string;
done: boolean;
}
const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (updatedTodo: Todo) => {
const response = await fetch(`/api/todos/${updatedTodo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTodo),
});
if (!response.ok) throw new Error('Update failed');
return response.json();
},
onMutate: async (updatedTodo) => {
// Cancel queries to prevent race conditions
await queryClient.cancelQueries({ queryKey: ['todos'] });
await queryClient.cancelQueries({ queryKey: ['todo', updatedTodo.id] });
// Snapshot current state
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
const previousTodo = queryClient.getQueryData<Todo>(['todo', updatedTodo.id]);
// Optimistically update list
if (previousTodos) {
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
);
}
// Optimistically update detail
queryClient.setQueryData(['todo', updatedTodo.id], updatedTodo);
return { previousTodos, previousTodo };
},
onError: (err, updatedTodo, context) => {
// Rollback on error
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
if (context?.previousTodo) {
queryClient.setQueryData(['todo', updatedTodo.id], context.previousTodo);
}
},
onSettled: (data, error, variables) => {
// Always refetch to ensure sync
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todo', variables.id] });
},
});
};
// Usage
function TodoItem({ todo }: { todo: Todo }) {
const updateTodo = useUpdateTodo();
const toggleDone = () => {
updateTodo.mutate({ ...todo, done: !todo.done });
};
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={toggleDone}
disabled={updateTodo.isPending}
/>
{todo.title}
</div>
);
}
```
---
## Pagination
### useInfiniteQuery (Infinite Scroll)
**Basic Infinite Query:**
```tsx
import { useInfiniteQuery } from '@tanstack/react-query';
interface PostsResponse {
posts: Post[];
nextCursor?: number;
}
function InfinitePosts() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json() as Promise<PostsResponse>;
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
);
}
```
**Bi-directional Pagination:**
```tsx
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json();
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
initialPageParam: 0,
});
```
**Infinite Scroll with Intersection Observer:**
```tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
function AutoLoadPosts() {
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
// Auto-fetch when sentinel comes into view
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
))}
{/* Sentinel element */}
<div ref={ref}>
{isFetchingNextPage && <Spinner />}
</div>
</div>
);
}
```
### Traditional Pagination
**Page-Based Pagination:**
```tsx
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: (previousData) => previousData, // Keep previous data while loading
});
return (
<div>
{isLoading ? (
<Spinner />
) : (
<div>
{data.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
)}
<div>
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => setPage(old => old + 1)}
disabled={!data?.hasMore}
>
Next
</button>
</div>
</div>
);
}
```
**Prefetch Next Page:**
```tsx
function PaginatedPosts() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
});
// Prefetch next page
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}
}, [data, page, queryClient]);
return (
<div>
{/* ... */}
</div>
);
}
```
---
## Cache Management
### Query Client Methods
**getQueryData (Read Cache):**
```tsx
const todos = queryClient.getQueryData<Todo[]>(['todos']);
const user = queryClient.getQueryData<User>(['user', userId]);
```
**setQueryData (Write Cache):**
```tsx
queryClient.setQueryData(['user', 1], newUser);
// Updater function
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [...old, newTodo]);
```
**invalidateQueries (Mark Stale + Refetch):**
```tsx
// Invalidate all queries
queryClient.invalidateQueries();
// Invalidate by key prefix
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Exact match only
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// With refetch control
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
});
```
**refetchQueries (Immediate Refetch):**
```tsx
// Refetch all active queries
await queryClient.refetchQueries();
// Refetch specific queries
await queryClient.refetchQueries({ queryKey: ['todos'] });
// Refetch with filters
await queryClient.refetchQueries({
queryKey: ['todos'],
type: 'active', // Only refetch active queries
});
```
**removeQueries (Delete from Cache):**
```tsx
// Remove all queries
queryClient.removeQueries();
// Remove specific
queryClient.removeQueries({ queryKey: ['todos', 1] });
// Remove with predicate
queryClient.removeQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.isArchived === true,
});
```
**resetQueries (Reset to Initial State):**
```tsx
// Reset all queries
queryClient.resetQueries();
// Reset specific
queryClient.resetQueries({ queryKey: ['todos'] });
```
### Cache Configuration
**Global Defaults:**
```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
refetchOnReconnect: true,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
mutations: {
retry: 1,
},
},
});
```
**Per-Query Configuration:**
```tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: Infinity, // Never mark stale
gcTime: Infinity, // Never garbage collect
refetchInterval: 5000, // Refetch every 5s
refetchIntervalInBackground: false, // Don't refetch when tab inactive
});
```
### Cache Persistence
**Persist to LocalStorage:**
```tsx
import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
```
**IndexedDB Persistence:**
```tsx
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';
const persister = createAsyncStoragePersister({
storage: {
getItem: async (key) => await get(key),
setItem: async (key, value) => await set(key, value),
removeItem: async (key) => await del(key),
},
});
```
---
## Error Handling and Retry
### Error Handling
**Query Error Boundaries:**
```tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Component />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
// Component throws errors to boundary
function Component() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
throwOnError: true, // Throw errors to error boundary
});
return <div>{data.name}</div>;
}
```
**Custom Error Types:**
```tsx
class APIError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}
const { error } = useQuery({
queryKey: ['user'],
queryFn: async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new APIError(
'Failed to fetch user',
response.status,
await response.text()
);
}
return response.json();
},
});
if (error instanceof APIError) {
if (error.status === 404) return <NotFound />;
if (error.status === 401) return <Unauthorized />;
}
```
### Retry Logic
**Default Retry:**
```tsx
// Retries 3 times with exponential backoff
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3, // Number of retries
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});
```
**Conditional Retry:**
```tsx
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: (failureCount, error) => {
// Don't retry on 404
if (error instanceof APIError && error.status === 404) {
return false;
}
// Retry up to 3 times for other errors
return failureCount < 3;
},
});
```
**Mutation Retry:**
```tsx
useMutation({
mutationFn: createUser,
retry: 2, // Retry mutations (use sparingly)
retryDelay: 1000,
});
```
### Network Status Detection
**Online/Offline Handling:**
```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 'online' | 'always' | 'offlineFirst'
refetchOnReconnect: true,
},
},
});
// Custom online/offline indicator
function OnlineStatus() {
const queryClient = useQueryClient();
const isOnline = useOnlineManager().isOnline();
useEffect(() => {
if (isOnline) {
queryClient.refetchQueries();
}
}, [isOnline, queryClient]);
return isOnline ? <OnlineIcon /> : <OfflineIcon />;
}
```
---
## SSR and Hydration
### Next.js App Router
**Server Component Data Fetching:**
```tsx
// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { UsersList } from './UsersList';
export default async function UsersPage() {
const queryClient = new QueryClient();
// Prefetch on server
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UsersList />
</HydrationBoundary>
);
}
```
**Client Component:**
```tsx
// app/users/UsersList.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
export function UsersList() {
// Uses hydrated data from server
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
```
### Next.js Pages Router
**getServerSideProps:**
```tsx
import { dehydrate, QueryClient } from '@tanstack/react-query';
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return {
prop...
© bobmatnyc · MIT · bobmatnyc/claude-mpm-skills
Похожие скиллы
Всё по теме «Фронтенд и UI» →Сначала — tanstack-query от других авторов.