--- name: tanstack-query description: TanStack React Query patterns - use for data fetching, caching, mutations, optimistic updates, and server state management --- # TanStack React Query Patterns ## Setup ```tsx // providers/QueryProvider.tsx 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { useState } from 'react' export function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 minute gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime) refetchOnWindowFocus: false, retry: 1, }, }, }) ) return ( {children} ) } ``` ## Query Keys ```tsx // lib/queryKeys.ts export const queryKeys = { environments: { all: ['environments'] as const, lists: () => [...queryKeys.environments.all, 'list'] as const, list: (filters: EnvironmentFilters) => [...queryKeys.environments.lists(), filters] as const, details: () => [...queryKeys.environments.all, 'detail'] as const, detail: (id: string) => [...queryKeys.environments.details(), id] as const, }, users: { all: ['users'] as const, detail: (id: string) => [...queryKeys.users.all, id] as const, }, } ``` ## Basic Queries ```tsx // hooks/useEnvironments.ts import { useQuery } from '@tanstack/react-query' import { queryKeys } from '@/lib/queryKeys' interface EnvironmentFilters { status?: string page?: number } async function fetchEnvironments(filters: EnvironmentFilters) { const params = new URLSearchParams() if (filters.status) params.set('status', filters.status) if (filters.page) params.set('page', String(filters.page)) const res = await fetch(`/api/environments?${params}`) if (!res.ok) throw new Error('Failed to fetch environments') return res.json() } export function useEnvironments(filters: EnvironmentFilters = {}) { return useQuery({ queryKey: queryKeys.environments.list(filters), queryFn: () => fetchEnvironments(filters), }) } // Usage function EnvironmentList() { const { data, isLoading, error } = useEnvironments({ status: 'RUNNING' }) if (isLoading) return if (error) return return ( ) } ``` ## Single Item Query ```tsx // hooks/useEnvironment.ts export function useEnvironment(id: string) { return useQuery({ queryKey: queryKeys.environments.detail(id), queryFn: async () => { const res = await fetch(`/api/environments/${id}`) if (!res.ok) { if (res.status === 404) return null throw new Error('Failed to fetch environment') } return res.json() }, enabled: !!id, // Don't fetch if no id }) } ``` ## Mutations ```tsx // hooks/useCreateEnvironment.ts import { useMutation, useQueryClient } from '@tanstack/react-query' import { queryKeys } from '@/lib/queryKeys' interface CreateEnvironmentInput { name: string description?: string } export function useCreateEnvironment() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (input: CreateEnvironmentInput) => { const res = await fetch('/api/environments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }) if (!res.ok) { const error = await res.json() throw new Error(error.message || 'Failed to create environment') } return res.json() }, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: queryKeys.environments.lists(), }) }, }) } // Usage function CreateEnvironmentForm() { const mutation = useCreateEnvironment() const handleSubmit = (e: React.FormEvent) => { e.preventDefault() const formData = new FormData(e.currentTarget) mutation.mutate({ name: formData.get('name') as string, description: formData.get('description') as string, }) } return (