; // No conditional on isFetching
```
**Status**: Known issue, being investigated by maintainers. Requires implementation of `getServerSnapshot` in useSyncExternalStore.
### Issue #10: useQuery Hydration Error with Prefetching
**Error**: Text content mismatch during hydration
**Source**: [GitHub Issue #9399](https://github.com/TanStack/query/issues/9399)
**Affects**: v5.x with server-side prefetching
**Why It Happens**: `tryResolveSync` detects resolved promises in RSC payload and extracts data synchronously during hydration, bypassing normal pending state
**Prevention**: Use `useSuspenseQuery` instead of `useQuery` for SSR, or avoid conditional rendering based on `isLoading`
**Before (causes hydration error):**
```tsx
// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Client Component
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return
Loading...
; // Server renders this
return
{data.length} todos
; // Client hydrates with this
}
```
**After (workaround):**
```tsx
// Use useSuspenseQuery instead
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return
{data.length} todos
;
}
```
**Status**: "At the top of my OSS list of things to fix" - maintainer Ephem (Nov 2025). Requires implementing `getServerSnapshot` in useSyncExternalStore.
### Issue #11: refetchOnMount Not Respected for Errored Queries
**Error**: Queries refetch on mount despite `refetchOnMount: false`
**Source**: [GitHub Issue #10018](https://github.com/TanStack/query/issues/10018)
**Affects**: v5.90.16+
**Why It Happens**: Errored queries with no data are always treated as stale. This is intentional to avoid permanently showing error states
**Prevention**: Use `retryOnMount: false` instead of (or in addition to) `refetchOnMount: false`
**Before (refetches despite setting):**
```tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('Fails') },
refetchOnMount: false, // Ignored when query is in error state
retry: 0,
});
// Query refetches every time component mounts
```
**After (correct):**
```tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch on mount for errored queries
retry: 0,
});
```
**Status**: Documented behavior (intentional). The name `retryOnMount` is slightly misleading - it controls whether errored queries trigger a new fetch on mount, not automatic retries.
### Issue #12: Mutation Callback Signature Breaking Change (v5.89.0)
**Error**: TypeScript errors in mutation callbacks
**Source**: [GitHub Issue #9660](https://github.com/TanStack/query/issues/9660)
**Affects**: v5.89.0+
**Why It Happens**: `onMutateResult` parameter added between `variables` and `context`, changing callback signatures from 3 params to 4
**Prevention**: Update all mutation callbacks to accept 4 parameters instead of 3
**Before (v5.88 and earlier):**
```tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context is now onMutateResult, missing final context param
},
onSuccess: (data, variables, context) => {
// Same issue
}
});
```
**After (v5.89.0+):**
```tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = return value from onMutate
// context = mutation function context
},
onSuccess: (data, variables, onMutateResult, context) => {
// Correct signature with 4 parameters
}
});
```
**Note**: If you don't use `onMutate`, the `onMutateResult` parameter will be undefined. This breaking change was introduced in a patch version.
### Issue #13: Readonly Query Keys Break Partial Matching (v5.90.8)
**Error**: `Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'`
**Source**: [GitHub Issue #9871](https://github.com/TanStack/query/issues/9871) | Fixed in [PR #9872](https://github.com/TanStack/query/pull/9872)
**Affects**: v5.90.8 only (fixed in v5.90.9)
**Why It Happens**: Partial query matching broke TypeScript types for readonly query keys (using `as const`)
**Prevention**: Upgrade to v5.90.9+ or use type assertions if stuck on v5.90.8
**Before (v5.90.8 - TypeScript error):**
```tsx
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']
useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// Error: readonly ['todos', string] not assignable to ['todos', string]
});
}
});
```
**After (v5.90.9+):**
```tsx
// Works correctly with readonly types
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ No type error
});
```
**Status**: Fixed in v5.90.9. Particularly affected users of code generators like `openapi-react-query` that produce readonly query keys.
### Issue #14: useMutationState Type Inference Lost
**Error**: `mutation.state.variables` typed as `unknown` instead of actual type
**Source**: [GitHub Issue #9825](https://github.com/TanStack/query/issues/9825)
**Affects**: All v5.x versions
**Why It Happens**: Fuzzy mutation key matching prevents guaranteed type inference (same issue as `queryClient.getQueryCache().find()`)
**Prevention**: Explicitly cast types in the `select` callback
**Before (type inference doesn't work):**
```tsx
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // Type: unknown
},
});
```
**After (with explicit cast):**
```tsx
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState
```
**Status**: Known limitation of fuzzy matching. No planned fix.
### Issue #15: Query Cancellation in StrictMode with fetchQuery
**Error**: `CancelledError` when using `fetchQuery()` with `useQuery`
**Source**: [GitHub Issue #9798](https://github.com/TanStack/query/issues/9798)
**Affects**: Development only (React StrictMode)
**Why It Happens**: StrictMode causes double mount/unmount. When `useQuery` unmounts and is the last observer, it cancels the query even if `fetchQuery()` is also running
**Prevention**: This is expected development-only behavior. Doesn't affect production
**Example:**
```tsx
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('Loaded:', data); // Never logs in StrictMode
} catch (error) {
console.error('Failed:', error); // CancelledError
}
}
function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// In StrictMode, component unmounts/remounts, cancelling fetchQuery
}
```
**Workaround:**
```tsx
// Keep query observed with staleTime
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // Keeps query active
});
```
**Status**: Expected StrictMode behavior, not a bug. Production builds are unaffected.
### Issue #16: invalidateQueries Only Refetches Active Queries
**Error**: Inactive queries not refetching despite `invalidateQueries()` call
**Source**: [GitHub Issue #9531](https://github.com/TanStack/query/issues/9531)
**Affects**: All v5.x versions
**Why It Happens**: Documentation was misleading - `invalidateQueries()` only refetches "active" queries by default, not "all" queries
**Prevention**: Use `refetchType: 'all'` to force refetch of inactive queries
**Default behavior:**
```tsx
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
```
**To refetch inactive queries:**
```tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // Refetch active AND inactive
});
```
**Status**: Documentation fixed to clarify "active" queries. This is the intended behavior.
---
## Community Tips
> **Note**: These tips come from community experts and maintainer blogs. Verify against your version.
### Tip: Query Options with Multiple Listeners
**Source**: [TkDodo's Blog - API Design Lessons](https://tkdodo.eu/blog/react-query-api-design-lessons-learned) | **Confidence**: HIGH
**Applies to**: v5.27.3+
When multiple components use the same query with different options (like `staleTime`), the "last write wins" rule applies for future fetches, but the current in-flight query uses its original options. This can cause unexpected behavior when components mount at different times.
**Example of unexpected behavior:**
```tsx
// Component A mounts first
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Applied initially
});
}
// Component B mounts while A's query is in-flight
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // Won't affect current fetch, only future ones
});
}
```
**Recommended approach:**
```tsx
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // Evaluated on each render
});
```
### Tip: refetch() is NOT for Changed Parameters
**Source**: [Avoiding Common Mistakes with TanStack Query](https://www.buncolak.com/posts/avoiding-common-mistakes-with-tanstack-query-part-1/) | **Confidence**: HIGH
The `refetch()` function should ONLY be used for refreshing with the same parameters (like a manual "reload" button). For new parameters (filters, page numbers, search terms, etc.), include them in the query key instead.
**Anti-pattern:**
```tsx
// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // Same key for all pages
queryFn: () => fetchTodos(page),
});
// This refetches with OLD page value, not new one
```
**Correct pattern:**
```tsx
// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page], // Key changes with page
queryFn: () => fetchTodos(page),
// Query automatically refetches when page changes
});
// Just update state
```
**When to use refetch():**
```tsx
// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
// Same parameters
```
---
## Key Patterns
**Dependent Queries** (Query B waits for Query A):
```tsx
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})
```
**Parallel Queries** (fetch multiple at once):
```tsx
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})
```
**Prefetching** (preload on hover):
```tsx
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
```
**Infinite Scroll** (useInfiniteQuery):
```tsx
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
```
**Query Cancellation** (auto-cancel on queryKey change):
```tsx
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos?q=${search}`, { signal })
return res.json()
}
```
**Data Transformation** (select):
```tsx
select: (data) => data.filter(todo => todo.completed)
```
**Avoid Request Waterfalls**: Fetch in parallel when possible (don't chain queries unless truly dependent)
---
**Official Docs**: https://tanstack.com/query/latest | **v5 Migration**: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | **GitHub**: https://github.com/TanStack/query | **Context7**: `/websites/tanstack_query`