613 lines
14 KiB
Markdown
613 lines
14 KiB
Markdown
---
|
|
name: tanstack-query
|
|
description: Master TanStack Query (React Query) v5 for server state management in React applications. Use when fetching data from APIs, managing server state, caching, or handling mutations. Triggers on phrases like "react query", "tanstack query", "data fetching", "cache management", "server state", or file patterns like *query*.ts, *Query*.tsx, queryClient.ts.
|
|
---
|
|
|
|
# TanStack Query (React Query) v5
|
|
|
|
Powerful asynchronous state management for React. TanStack Query makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Fetching data from REST APIs or GraphQL endpoints
|
|
- Managing server state and cache lifecycle
|
|
- Implementing mutations (create, update, delete operations)
|
|
- Building infinite scroll or load-more patterns
|
|
- Handling optimistic UI updates
|
|
- Synchronizing data across components
|
|
- Implementing background data refetching
|
|
- Managing complex async state without Redux or other state managers
|
|
|
|
## Quick Start Workflow
|
|
|
|
### 1. Installation
|
|
|
|
```bash
|
|
npm install @tanstack/react-query
|
|
# or
|
|
pnpm add @tanstack/react-query
|
|
# or
|
|
yarn add @tanstack/react-query
|
|
```
|
|
|
|
### 2. Setup QueryClient
|
|
|
|
Wrap your application with `QueryClientProvider`:
|
|
|
|
```tsx
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
function App() {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<YourApp />
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3. Basic Query
|
|
|
|
```tsx
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
function TodoList() {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: async () => {
|
|
const res = await fetch('https://api.example.com/todos');
|
|
if (!res.ok) throw new Error('Network response was not ok');
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
if (isLoading) return <div>Loading...</div>;
|
|
if (error) return <div>Error: {error.message}</div>;
|
|
|
|
return (
|
|
<ul>
|
|
{data.map((todo) => (
|
|
<li key={todo.id}>{todo.title}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 4. Basic Mutation
|
|
|
|
```tsx
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
function CreateTodo() {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async (newTodo) => {
|
|
const res = await fetch('https://api.example.com/todos', {
|
|
method: 'POST',
|
|
body: JSON.stringify(newTodo),
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
// Invalidate and refetch todos
|
|
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
},
|
|
});
|
|
|
|
return (
|
|
<button onClick={() => mutation.mutate({ title: 'New Todo' })}>
|
|
{mutation.isPending ? 'Creating...' : 'Create Todo'}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### Query Keys
|
|
|
|
Query keys uniquely identify queries and are used for caching. They must be arrays.
|
|
|
|
```tsx
|
|
// Simple key
|
|
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
|
|
|
|
// Key with variables
|
|
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });
|
|
|
|
// Hierarchical keys
|
|
useQuery({ queryKey: ['todos', 'list', { filters, page }], queryFn: fetchTodos });
|
|
```
|
|
|
|
**Query key matching:**
|
|
- `['todos']` - exact match
|
|
- `['todos', { page: 1 }]` - exact match with object
|
|
- `{ queryKey: ['todos'] }` - matches all queries starting with 'todos'
|
|
|
|
### Query Functions
|
|
|
|
Query functions must return a promise that resolves data or throws an error:
|
|
|
|
```tsx
|
|
// Using fetch
|
|
queryFn: async () => {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error('Failed to fetch');
|
|
return res.json();
|
|
}
|
|
|
|
// Using axios
|
|
queryFn: () => axios.get(url).then(res => res.data)
|
|
|
|
// With query key access
|
|
queryFn: ({ queryKey }) => {
|
|
const [_, todoId] = queryKey;
|
|
return fetchTodo(todoId);
|
|
}
|
|
```
|
|
|
|
### Important Defaults
|
|
|
|
Understanding defaults is crucial for optimal usage:
|
|
|
|
- **staleTime: 0** - Queries become stale immediately by default
|
|
- **gcTime: 5 minutes** - Unused/inactive cache data remains in memory for 5 minutes
|
|
- **retry: 3** - Failed queries retry 3 times with exponential backoff
|
|
- **refetchOnWindowFocus: true** - Queries refetch when window regains focus
|
|
- **refetchOnReconnect: true** - Queries refetch when network reconnects
|
|
|
|
```tsx
|
|
// Override defaults globally
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
gcTime: 1000 * 60 * 10, // 10 minutes
|
|
},
|
|
},
|
|
});
|
|
|
|
// Or per query
|
|
useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
staleTime: 1000 * 60, // 1 minute
|
|
retry: 5,
|
|
});
|
|
```
|
|
|
|
### Query Status and Fetch Status
|
|
|
|
Queries have two important states:
|
|
|
|
**Query Status:**
|
|
- `pending` - No cached data, query is executing
|
|
- `error` - Query encountered an error
|
|
- `success` - Query succeeded and data is available
|
|
|
|
**Fetch Status:**
|
|
- `fetching` - Query function is executing
|
|
- `paused` - Query wants to fetch but is paused (offline)
|
|
- `idle` - Query is not fetching
|
|
|
|
```tsx
|
|
const { data, status, fetchStatus, isLoading, isFetching } = useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
});
|
|
|
|
// isLoading = status === 'pending'
|
|
// isFetching = fetchStatus === 'fetching'
|
|
```
|
|
|
|
### Query Invalidation
|
|
|
|
Mark queries as stale to trigger refetches:
|
|
|
|
```tsx
|
|
const queryClient = useQueryClient();
|
|
|
|
// Invalidate all todos queries
|
|
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
|
|
// Invalidate specific query
|
|
queryClient.invalidateQueries({ queryKey: ['todo', todoId] });
|
|
|
|
// Invalidate and refetch immediately
|
|
queryClient.invalidateQueries({
|
|
queryKey: ['todos'],
|
|
refetchType: 'active' // only refetch active queries
|
|
});
|
|
```
|
|
|
|
### Mutations
|
|
|
|
Mutations are used for creating, updating, or deleting data:
|
|
|
|
```tsx
|
|
const mutation = useMutation({
|
|
mutationFn: (newTodo) => {
|
|
return fetch('/api/todos', {
|
|
method: 'POST',
|
|
body: JSON.stringify(newTodo),
|
|
});
|
|
},
|
|
onSuccess: (data, variables, context) => {
|
|
console.log('Success!', data);
|
|
},
|
|
onError: (error, variables, context) => {
|
|
console.error('Error:', error);
|
|
},
|
|
onSettled: (data, error, variables, context) => {
|
|
console.log('Mutation finished');
|
|
},
|
|
});
|
|
|
|
// Trigger mutation
|
|
mutation.mutate({ title: 'New Todo' });
|
|
|
|
// With async/await
|
|
mutation.mutateAsync({ title: 'New Todo' })
|
|
.then(data => console.log(data))
|
|
.catch(error => console.error(error));
|
|
```
|
|
|
|
### React Suspense Integration
|
|
|
|
TanStack Query supports React Suspense with dedicated hooks:
|
|
|
|
```tsx
|
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
|
|
function TodoList() {
|
|
// This will suspend the component until data is ready
|
|
const { data } = useSuspenseQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
});
|
|
|
|
// No need for loading states - handled by Suspense boundary
|
|
return (
|
|
<ul>
|
|
{data.map((todo) => (
|
|
<li key={todo.id}>{todo.title}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
// In parent component
|
|
function App() {
|
|
return (
|
|
<Suspense fallback={<div>Loading todos...</div>}>
|
|
<TodoList />
|
|
</Suspense>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Advanced Topics
|
|
|
|
For detailed information on advanced patterns, see the reference files:
|
|
|
|
### Infinite Queries
|
|
|
|
For implementing infinite scroll and load-more patterns:
|
|
- See `references/infinite-queries.md` for comprehensive guide
|
|
- Covers `useInfiniteQuery` hook
|
|
- Bidirectional pagination
|
|
- `getNextPageParam` and `getPreviousPageParam`
|
|
- Refetching and background updates
|
|
|
|
### Optimistic Updates
|
|
|
|
For updating UI before server confirmation:
|
|
- See `references/optimistic-updates.md` for detailed patterns
|
|
- Optimistic mutations
|
|
- Rollback on error
|
|
- Context for cancellation
|
|
- UI feedback strategies
|
|
|
|
### TypeScript Support
|
|
|
|
For full type safety and inference:
|
|
- See `references/typescript.md` for complete TypeScript guide
|
|
- Type inference from query functions
|
|
- Generic type parameters
|
|
- Typing query options
|
|
- Custom hooks with types
|
|
- Error type narrowing
|
|
|
|
### Query Invalidation Patterns
|
|
|
|
For advanced cache invalidation strategies:
|
|
- See `references/query-invalidation.md`
|
|
- Partial matching
|
|
- Predicate functions
|
|
- Refetch strategies
|
|
- Query filters
|
|
|
|
### Performance Optimization
|
|
|
|
For optimizing query performance:
|
|
- See `references/performance.md`
|
|
- Query deduplication
|
|
- Structural sharing
|
|
- Memory management
|
|
- Query splitting strategies
|
|
|
|
## DevTools
|
|
|
|
TanStack Query DevTools provide visual insights into query state:
|
|
|
|
```bash
|
|
npm install @tanstack/react-query-devtools
|
|
```
|
|
|
|
```tsx
|
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
|
|
function App() {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<YourApp />
|
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
**DevTools features:**
|
|
- View all queries and their states
|
|
- Inspect query data and errors
|
|
- Manually trigger refetches
|
|
- Invalidate queries
|
|
- Monitor cache lifecycle
|
|
|
|
## Common Patterns
|
|
|
|
### Dependent Queries
|
|
|
|
Run queries in sequence when one depends on another:
|
|
|
|
```tsx
|
|
// First query
|
|
const { data: user } = useQuery({
|
|
queryKey: ['user', userId],
|
|
queryFn: () => fetchUser(userId),
|
|
});
|
|
|
|
// Second query depends on first
|
|
const { data: projects } = useQuery({
|
|
queryKey: ['projects', user?.id],
|
|
queryFn: () => fetchProjects(user.id),
|
|
enabled: !!user?.id, // Only run when user.id is available
|
|
});
|
|
```
|
|
|
|
### Parallel Queries
|
|
|
|
Multiple independent queries in one component:
|
|
|
|
```tsx
|
|
function Dashboard() {
|
|
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
|
|
const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
|
|
const stats = useQuery({ queryKey: ['stats'], queryFn: fetchStats });
|
|
|
|
if (users.isLoading || posts.isLoading || stats.isLoading) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
// All queries succeeded
|
|
return <DashboardView users={users.data} posts={posts.data} stats={stats.data} />;
|
|
}
|
|
```
|
|
|
|
### Dynamic Parallel Queries
|
|
|
|
Use `useQueries` for dynamic number of queries:
|
|
|
|
```tsx
|
|
import { useQueries } from '@tanstack/react-query';
|
|
|
|
function TodoLists({ listIds }) {
|
|
const results = useQueries({
|
|
queries: listIds.map((id) => ({
|
|
queryKey: ['list', id],
|
|
queryFn: () => fetchList(id),
|
|
})),
|
|
});
|
|
|
|
const isLoading = results.some(result => result.isLoading);
|
|
const data = results.map(result => result.data);
|
|
|
|
return <Lists data={data} />;
|
|
}
|
|
```
|
|
|
|
### Prefetching
|
|
|
|
Prefetch data before it's needed:
|
|
|
|
```tsx
|
|
const queryClient = useQueryClient();
|
|
|
|
// Prefetch on hover
|
|
function TodoListLink({ id }) {
|
|
const prefetch = () => {
|
|
queryClient.prefetchQuery({
|
|
queryKey: ['todo', id],
|
|
queryFn: () => fetchTodo(id),
|
|
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Link to={`/todo/${id}`} onMouseEnter={prefetch}>
|
|
View Todo
|
|
</Link>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Initial Data
|
|
|
|
Provide initial data to avoid loading states:
|
|
|
|
```tsx
|
|
function TodoDetail({ todoId, initialTodo }) {
|
|
const { data } = useQuery({
|
|
queryKey: ['todo', todoId],
|
|
queryFn: () => fetchTodo(todoId),
|
|
initialData: initialTodo, // Use this data immediately
|
|
staleTime: 1000 * 60, // Consider fresh for 1 minute
|
|
});
|
|
|
|
return <div>{data.title}</div>;
|
|
}
|
|
```
|
|
|
|
### Placeholder Data
|
|
|
|
Show placeholder while loading:
|
|
|
|
```tsx
|
|
const { data, isPlaceholderData } = useQuery({
|
|
queryKey: ['todos', page],
|
|
queryFn: () => fetchTodos(page),
|
|
placeholderData: (previousData) => previousData, // Keep previous data while loading
|
|
});
|
|
|
|
// Or use static placeholder
|
|
const { data } = useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
placeholderData: { items: [], total: 0 },
|
|
});
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Query Errors
|
|
|
|
```tsx
|
|
const { error, isError } = useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
retry: 3,
|
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
});
|
|
|
|
if (isError) {
|
|
return <div>Error: {error.message}</div>;
|
|
}
|
|
```
|
|
|
|
### Global Error Handling
|
|
|
|
```tsx
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
onError: (error) => {
|
|
console.error('Query error:', error);
|
|
// Show toast notification, etc.
|
|
},
|
|
},
|
|
mutations: {
|
|
onError: (error) => {
|
|
console.error('Mutation error:', error);
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### Error Boundaries
|
|
|
|
Combine with React Error Boundaries:
|
|
|
|
```tsx
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
|
|
function TodoList() {
|
|
const { data } = useQuery({
|
|
queryKey: ['todos'],
|
|
queryFn: fetchTodos,
|
|
throwOnError: true, // Throw errors to error boundary
|
|
});
|
|
|
|
return <div>{/* render data */}</div>;
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
<TodoList />
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Use Query Keys Wisely**
|
|
- Structure keys hierarchically: `['todos', 'list', { filters }]`
|
|
- Include all variables in the key
|
|
- Keep keys consistent across your app
|
|
|
|
2. **Set Appropriate staleTime**
|
|
- Static data: `staleTime: Infinity`
|
|
- Frequently changing: `staleTime: 0` (default)
|
|
- Moderately changing: `staleTime: 1000 * 60 * 5` (5 minutes)
|
|
|
|
3. **Handle Loading and Error States**
|
|
- Always check `isLoading` and `error`
|
|
- Provide meaningful loading indicators
|
|
- Show user-friendly error messages
|
|
|
|
4. **Optimize Refetching**
|
|
- Disable unnecessary refetches with `refetchOnWindowFocus: false`
|
|
- Use `staleTime` to reduce refetches
|
|
- Consider using `refetchInterval` for polling
|
|
|
|
5. **Invalidate Efficiently**
|
|
- Invalidate specific queries, not all queries
|
|
- Use query key prefixes for related queries
|
|
- Combine with optimistic updates for better UX
|
|
|
|
6. **Use TypeScript**
|
|
- Type your query functions for type inference
|
|
- Use generic type parameters when needed
|
|
- Enable strict type checking
|
|
|
|
7. **Leverage DevTools**
|
|
- Install DevTools in development
|
|
- Monitor query behavior
|
|
- Debug cache issues
|
|
|
|
## Resources
|
|
|
|
- **Official Documentation**: https://tanstack.com/query/latest/docs/framework/react/overview
|
|
- **GitHub Repository**: https://github.com/TanStack/query
|
|
- **Examples**: https://tanstack.com/query/latest/docs/framework/react/examples
|
|
- **Community**: https://discord.gg/tanstack
|
|
- **TypeScript Guide**: https://tanstack.com/query/latest/docs/framework/react/typescript
|
|
|
|
## Migration from v4
|
|
|
|
If you're upgrading from React Query v4:
|
|
|
|
- `cacheTime` renamed to `gcTime`
|
|
- `useInfiniteQuery` pageParam changes
|
|
- New `useSuspenseQuery` hooks
|
|
- Improved TypeScript inference
|
|
- See official migration guide: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
|