Core Patterns
Prefer data-first rendering when stale data is useful. A failed background refetch can produce isError while data is still available.
const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
if (todos.data)
return <TodoList todos={todos.data} isRefreshing={todos.isFetching} />
if (todos.isPending) return <Spinner />
if (todos.isError) return <ErrorMessage error={todos.error} />
return null
Use throwOnError when render-time Error Boundaries should own the fallback:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: (error) => error.status >= 500,
})
Use global cache callbacks for cross-cutting notifications:
import { QueryCache, QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (query.state.data !== undefined) showToast(error.message)
},
}),
})
Common Mistakes
HIGH Hiding stale data on background error
Wrong:
if (query.isError) return <ErrorMessage error={query.error} />
if (query.data) return <Todos todos={query.data} />
Correct:
if (query.data)
return (
<Todos todos={query.data} staleError={query.isError ? query.error : null} />
)
if (query.isError) return <ErrorMessage error={query.error} />
Background refetch failures should not necessarily erase already-rendered data.
Source: https://tkdodo.eu/blog/status-checks-in-react-query
HIGH Sending validation errors to a global boundary
Wrong:
useMutation({ mutationFn: submitForm, throwOnError: true })
Correct:
useMutation({
mutationFn: submitForm,
throwOnError: (error) => error.status >= 500,
})
Handle expected 4xx validation errors near the form. Send unexpected server failures to the boundary.
Source: https://tkdodo.eu/blog/react-query-error-handling
MEDIUM Duplicating toast notifications per observer
Wrong:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onError: toastError,
})
Correct:
new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (query.state.data !== undefined) toastError(error)
},
}),
})
Observer-level callbacks can duplicate notifications across components. Use cache-level callbacks for global side effects.