tanstack-query
TanStack Query (React Query)
Powerful asynchronous state management for React - fetch, cache, synchronize and update server state.
Instructions
- Separate server state - Use React Query for server data, not local UI state
- Configure stale time - Set appropriate stale times based on data freshness needs
- Use query keys - Structure keys hierarchically for cache management
- Handle loading/error - Every query needs these states handled
- Invalidate strategically - Invalidate related queries after mutations
Setup
npm install @tanstack/react-query
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Basic Queries
Simple Query
import { useQuery } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
}
async function fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
function UserProfile({ userId }: { userId: string }) {
const {
data: user,
isLoading,
isError,
error,
refetch,
} = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <Skeleton />;
if (isError) return <Error message={error.message} onRetry={refetch} />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Query with Options
const { data } = useQuery({
queryKey: ['todos', { status: 'active' }],
queryFn: fetchActiveTodos,
staleTime: 1000 * 60 * 10, // Data fresh for 10 minutes
gcTime: 1000 * 60 * 30, // Cache for 30 minutes (formerly cacheTime)
refetchInterval: 1000 * 30, // Refetch every 30 seconds
refetchOnMount: true,
refetchOnWindowFocus: true,
enabled: !!userId, // Only run if userId exists
placeholderData: [], // Show while loading
select: (data) => data.filter(todo => !todo.completed), // Transform data
});
Mutations
Basic Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateTodoInput {
title: string;
completed: boolean;
}
function TodoForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo: CreateTodoInput) =>
fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
}).then(res => res.json()),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onError: (error) => {
toast.error(`Failed to create: ${error.message}`);
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
title: formData.get('title') as string,
completed: false,
});
}}>
<input name="title" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}
Optimistic Updates
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.map(todo =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
)
);
// Return snapshot for rollback
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Infinite Queries
import { useInfiniteQuery } from '@tanstack/react-query';
interface PostsPage {
posts: Post[];
nextCursor: string | null;
}
function PostsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam }): Promise<PostsPage> => {
const res = await fetch(`/api/posts?cursor=${pageParam}`);
return res.json();
},
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];
return (
<div>
{allPosts.map(post => (
<PostCard key={post.id} post={post} />
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
</button>
)}
</div>
);
}
Prefetching
import { useQueryClient } from '@tanstack/react-query';
function PostsList() {
const queryClient = useQueryClient();
const prefetchPost = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
staleTime: 1000 * 60 * 5,
});
};
return (
<ul>
{posts.map(post => (
<li
key={post.id}
onMouseEnter={() => prefetchPost(post.id)}
>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
Query Keys
Hierarchical Keys
// Structure keys for easy invalidation
const queryKeys = {
all: ['todos'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: Filters) => [...queryKeys.lists(), filters] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.details(), id] as const,
};
// Usage
useQuery({
queryKey: queryKeys.detail(todoId),
queryFn: () => fetchTodo(todoId),
});
// Invalidate all todos
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only lists
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });
Parallel & Dependent Queries
Parallel Queries
function Dashboard() {
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const projectsQuery = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
});
if (usersQuery.isLoading || projectsQuery.isLoading) {
return <Loading />;
}
return (
<>
<UsersList users={usersQuery.data} />
<ProjectsList projects={projectsQuery.data} />
</>
);
}
Dependent Queries
function UserPosts({ userId }: { userId: string }) {
// First query
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Dependent query - only runs when user exists
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user?.id, // Only run when user.id exists
});
return <PostsList posts={posts} />;
}
Error Handling
function ErrorBoundaryWithRetry({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div className="error-container">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Global error handling
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true, // Propagate to error boundary
},
mutations: {
onError: (error) => {
toast.error(`Error: ${error.message}`);
},
},
},
});
Best Practices
| Practice | Recommendation |
|---|---|
| Query Keys | Use arrays, structure hierarchically |
| Stale Time | Set based on data update frequency |
| Mutations | Invalidate related queries on success |
| Loading | Always handle isLoading state |
| Errors | Always handle isError with retry option |
| Devtools | Use in development for debugging |
When to Use
- Fetching data from REST/GraphQL APIs
- Server state management
- Real-time data synchronization
- Pagination and infinite scroll
- Optimistic updates
- Data prefetching
Notes
- TanStack Query v5 renamed cacheTime to gcTime
- Works with any fetching library (fetch, axios, etc.)
- Excellent TypeScript support
- Supports React, Vue, Solid, Svelte
- 25kb gzipped bundle size
More from housegarofalo/claude-code-base
mqtt-iot
Configure MQTT brokers (Mosquitto, EMQX) for IoT messaging, device communication, and smart home integration. Manage topics, QoS levels, authentication, and bridging. Use when setting up IoT messaging, smart home communication, or device-to-cloud connectivity. (project)
22home-assistant
Ultimate Home Assistant skill - complete administration, wireless protocols (Zigbee/ZHA/Z2M, Z-Wave JS, Thread, Matter), ESPHome device building, advanced troubleshooting, performance optimization, security hardening, custom integration development, and professional dashboard design. Covers configuration, REST API, automation debugging, database optimization, SSL/TLS, Jinja2 templating, and HACS custom cards. Use for any HA task.
6testing
Comprehensive testing skill covering unit, integration, and E2E testing with pytest, Jest, Cypress, and Playwright. Use for writing tests, improving coverage, debugging test failures, and setting up testing infrastructure.
5power-automate
Expert guidance for Power Automate development including cloud flows, desktop flows, Dataverse connector, expression functions, custom connectors, error handling, and child flow patterns. Use when building automated workflows, writing flow expressions, creating custom connectors from OpenAPI, or implementing error handling patterns.
5mobile-pwa
Build Progressive Web Apps with offline support, push notifications, and native-like experiences. Covers service workers, Web App Manifest, caching strategies, IndexedDB, background sync, and installability. Use for mobile-first web apps, offline-capable applications, and app-like experiences.
5svelte-kit
Expert guidance for SvelteKit 2 with Svelte 5 runes, server-side rendering, and modern patterns. Covers $state, $derived, $effect, form actions, load functions, and API routes. Use for SvelteKit applications, Svelte 5 runes, and full-stack Svelte development.
5