TanStack Query Best Practices
TanStack Query (formerly React Query) handles server-state caching, background updates, and stale-data management out of the box. This skill covers v5 patterns and APIs — v5 introduced several breaking changes from v4 that older examples online still don't reflect.
Core Principles
- Use TanStack Query for all server state management and data fetching; it is not a general client-state manager — keep client-only state in
useState/context/a state library instead - Minimize
useEffectanduseStatefor server data; favor TanStack Query's built-in state management - Every query needs a stable, serializable query key that uniquely describes the data it holds
- Mutations handle writes; queries handle reads — don't blur this boundary
- Implement proper error handling with user-friendly messages
- Use TypeScript for full type safety with query responses
v5 Breaking Changes to Watch For
If you see or write any of these v4 patterns, update them:
- Object syntax only:
useQuery,useInfiniteQuery, etc. no longer accept positional arguments (useQuery(key, fn, options)). Always pass a single options object:useQuery({ queryKey, queryFn, ...options }). isPendingreplacesisLoadingas the name for "no data yet and a fetch is in flight" onuseMutation. OnuseQuery,isPendingmeans no cached data exists at all;isLoadingis now derived (isPending && isFetching) and still usable for the classic "first load" spinner case.cacheTimerenamed togcTime(garbage collection time).queryOptions()helper for defining reusable, typed query definitions shared between components, loaders, and prefetch calls.useSuspenseQuery(anduseSuspenseInfiniteQuery) for Suspense-based data fetching, replacing the oldsuspense: trueoption.placeholderData: keepPreviousDatareplaces the oldkeepPreviousData: trueboolean for pagination.
Project Structure
Setup and Configuration
Query Client Configuration
Instantiate QueryClient once at the app root — never inside a component, or the cache resets on every render.
Query Best Practices
1. Query Key Organization
Use consistent, hierarchical query keys for efficient cache management:
2. queryOptions Helper (v5)
Define a query once with queryOptions() and reuse the same definition across components, router loaders, and prefetch calls — this keeps the query key, query function, and options in one place instead of duplicating them:
Always define queryOptions outside components — never inline a fresh object literal in every useQuery() call — so the definition can be shared and prefetched.
3. Custom Query Hooks
Create reusable, typed query hooks when a queryOptions() factory isn't reused elsewhere:
4. Dependent Queries
Handle queries that depend on other data:
5. Parallel Queries
Fetch multiple resources simultaneously:
Mutation Best Practices
1. Basic Mutations
isPending is the v5 name for "mutation in flight" (v4 called this isLoading on mutations too — that name is gone).
2. Optimistic Updates
Provide instant feedback while mutations are in flight:
3. Cache Invalidation
Properly invalidate related queries after mutations:
Other cache operations worth knowing:
Error Handling
1. Global Error Handler
2. Component-Level Error Handling
3. Conditional Retry Logic
Skip retries for errors that will never succeed on retry, like 404s:
4. Suspense Mode (v5)
Use useSuspenseQuery for Suspense-based data fetching instead of the old suspense: true option — it also narrows the return type since data can never be undefined:
Use throwOnError: true on a regular useQuery if you want errors to bubble to the nearest ErrorBoundary without switching to Suspense.
Performance Optimization
1. Select and Transform Data
Only subscribe to the data you need:
2. Prefetching
Prefetch data before it's needed — on hover, or during routing:
3. Infinite Queries
Handle paginated data efficiently:
Use placeholderData: keepPreviousData (imported from @tanstack/react-query) on paginated or filtered queries to keep showing the previous page's data while the next page loads, instead of flashing a loading state:
Use notifyOnChangeProps to limit re-renders to only the specific result properties a component actually reads.
TypeScript Tips
- Always type
queryFnreturn value explicitly, or infer it from typed API functions - Use
QueryObserverResult<TData, TError>to type hook return values - Use
UseMutationResult<TData, TError, TVariables>for mutations
Key Conventions
- Feature-based organization: Group query hooks and
queryOptionsfactories within feature-specific directories - Consistent query keys: Use factory functions for type-safe, organized keys
- queryOptions everywhere reusable: Prefer
queryOptions()over ad hoc inline options whenever a query is used in more than one place (component, loader, prefetch) - Type safety: Define TypeScript interfaces for all API responses
- DevTools: Always include React Query DevTools in development
- Avoid deeply nested queries: Flatten query structures when possible
- Fetch only needed data: Use API parameters to limit response size
- Handle loading and error states: Always provide appropriate UI feedback
Anti-Patterns to Avoid
- Do not use
useEffectto fetch data — use queries or router loaders instead - Do not store server state in local state (
useState) - Do not pass positional arguments to
useQuery/useInfiniteQuery— v5 requires the options-object form - Do not check
isLoadingalone on a mutation — useisPending - Do not forget to handle loading and error states
- Do not create overly specific query keys that prevent cache reuse
- Do not skip cache invalidation after mutations
- Do not ignore the
enabledoption for conditional queries - Do not define
queryOptions/query configs inline inside components when they're reused elsewhere — co-locate and share them


