Frontend
TanStack Query
Comprehensive TanStack Query best practices based on TkDodo's full series.
TanStack Query
TanStack Query is our default async server-state manager for React apps. This guide consolidates practical team conventions from TkDodo's complete series (all 32 entries, including #8a) and adapts them to TanStack Query naming and v5-style usage.
What it is
- Treat TanStack Query as server-state orchestration, not as a generic local state store.
- Keep server state and client state separate; copy server data into local state only when deliberate (for example, form initialization).
- Think declaratively: query keys drive fetches; avoid imperative "refetch with different params".
- Prefer tuning
staleTimeover disabling useful defaults like focus/reconnect refetch. - Use custom hooks or
queryOptionsfactories to keep query key + query function + options colocated.
Best practices
Why we use it
- Use array keys and structure from broad to specific, e.g.
['todos', 'list', filters],['todos', 'detail', id]. - Include every query dependency in the key.
- Never share the same key between
useQueryanduseInfiniteQuery. - Prefer key factories per feature and colocate them with query code.
- For complex apps, consider object-style keys for clearer fuzzy matching and context typing.
Setup in this repo
- Understand default behavior (
staleTime: 0, background refetch on mount/focus/reconnect). - Show stale data first when reasonable; avoid replacing useful stale UI with full-screen errors.
- Use
gcTimeas a cache cleanup control; change it rarely. - For websocket-driven or push-updated UIs, increase
staleTimesignificantly (sometimesInfinity) and invalidate explicitly on events. - Use
enabledfor dependent queries and "wait for user input" workflows.
Team conventions
- Prefer
selectfor data transformation and observer-level subscriptions. - Keep selectors stable (
useCallbackor extracted function) when transformations are expensive. - For heavy transforms used by many observers, add external memoization around the transform.
- Use
selectfor partial subscriptions to reduce rerenders without splitting cache entries.
Error handling and status checks
- Ensure query functions throw rejected promises for real failures (
fetchusers must throw on!response.ok). - In many UX flows, check for data first, then error, then pending/loading.
- Use
throwOnErrorwith Error Boundaries for coarse-grained handling. - Use global
QueryCache/MutationCachecallbacks for one-time toast/monitoring side effects.
Mutations and invalidation
- Prefer invalidation as the default post-mutation strategy; direct cache writes only when response data is authoritative and easy to merge.
- Keep mutation logic-side effects (invalidation/cache updates) in mutation callbacks; keep UI side effects near UI call sites.
- Return/await invalidation promises only when UX needs mutation to remain pending until refetch finishes.
- For optimistic updates, cancel overlapping queries before optimistic writes.
- In concurrent optimistic scenarios, avoid over-invalidation by skipping redundant invalidations when related mutations are still running.
TypeScript safety patterns
- Let inference work: type API layer return values, avoid manually supplying
useQuerygenerics unless required. - Narrow unknown errors (
instanceof Error) and runtime-validate API payloads for critical paths (for example with schemas). - Prefer
queryOptionsfor reusable type-safe query definitions. - Use strongly typed key/query factories for consistent key usage and safer
QueryFunctionContext.
Forms, router, offline, and infinite queries
- Router loaders + TanStack Query: pre-seed cache early in loaders, then read with query hooks in components.
- Forms: either
- treat server values as initial defaults and disable unnecessary background updates, or
- keep server + draft states separate and derive displayed values.
- Offline/network modes: choose
online,always, orofflineFirstintentionally per query/mutation behavior. - Seeding detail caches: use pull (
initialDatafrom list cache +initialDataUpdatedAt) or push approaches based on list size/churn.
Testing guidelines
- Use one
QueryClientper test for isolation. - Disable retries in tests unless explicitly testing retry behavior.
- Prefer network-level mocking (for example MSW) over client implementation mocking.
- Always await settled query states before asserting.
Abstractions and anti-patterns
- Keep TanStack Query config shared and stable via a single app-level
QueryClientProvider. - Use feature-local query factories (
queryOptions+ key helpers) instead of global key constants. - Default to invalidation over manual cache surgery.
- Use
selectonly when needed for real rendering/perf benefits. - Keep docs/code terminology consistent: prefer TanStack Query over legacy "React Query".
Example
import { useQuery } from "@tanstack/react-query";
export function useProjectsQuery() {
return useQuery({
queryKey: ["projects"],
queryFn: async () => {
const res = await fetch("/api/projects");
if (!res.ok) throw new Error("Failed to fetch projects");
return res.json();
},
});
}Common pitfalls
- Query keys and query function dependencies drifting out of sync.
- Treating query cache as local editable state.
- Overusing optimistic updates for complex flows.
- Recreating
QueryClientaccidentally and losing cache state. - Using
initialDatawhereplaceholderDatais intended. - Hiding fetch errors by returning resolved promises from
queryFncatch blocks.
References
Complete TkDodo series
Foundational Patterns
- #1: Practical React Query
- #2: React Query Data Transformations
- #3: React Query Render Optimizations
- #4: Status Checks in React Query
- #5: Testing React Query
- #6: React Query and TypeScript
- #7: Using WebSockets with React Query
Keys, Context, Data Seeding, and State Design
- #8: Effective React Query Keys
- #8a: Leveraging the Query Function Context
- #9: Placeholder and Initial Data in React Query
- #10: React Query as a State Manager
- #11: React Query Error Handling
- #12: Mastering Mutations in React Query
- #13: Offline React Query
- #14: React Query and Forms
- #15: React Query FAQs
- #16: React Query meets React Router
- #17: Seeding the Query Cache
- #18: Inside React Query
Advanced Type Safety, API Design, and Performance
- #19: Type-safe React Query
- #20: You Might Not Need React Query
- #21: Thinking in React Query
- #22: React Query and React Context
- #23: Why You Want React Query
- #24: The Query Options API
- #25: Automatic Query Invalidation after Mutations
- #26: How Infinite Queries work
- #27: React Query API Design - Lessons Learned
- #28: React Query - The Bad Parts
- #29: Concurrent Optimistic Updates in React Query
- #30: React Query Selectors, Supercharged
- #31: Creating Query Abstractions