When implementing or modifying caching/fetch decisions, treat “fresh vs stale” as a single source of truth derived from the query’s invalidation + stale-time logic. Avoid adding ad-hoc fetch conditions at the observer/queryClient level that bypass (or redefine) TTL semantics.
Concrete rules:
1) Model semantic events (e.g., “background error while data exists”) by updating invalidation state in the query/state layer (e.g., set isInvalidated: true).
isStale() / existing stale-time helpers, not on status === 'error' special-cases.
2) Preserve staleTime meaning.ensureQueryData/prefetchQuery should decide whether to return cached data now vs kick off a background fetch; they should not silently reinterpret the option (e.g., don’t turn “prefetch threshold” into “global query staleTime”).
3) If you implement TTL via timers:isInvalidated when staleTime changes.staleTime === 0, mark as invalidated deterministically.QueryClient and ensure SSR-derived state is sanitized so client hydration reuses the intended freshness state.Example pattern (central invalidation drives fetch):
// In reducer/state update (when an error happens during a background refetch
// and existing data remains):
return {
...state,
status: 'error',
isInvalidated: true, // centralize semantic freshness change here
}
// In fetch decision logic (observer/queryObserver):
const shouldFetch = value === 'always' || (value !== false && query.isStale())
Applying these rules prevents subtle regressions like staleTime: 'static' data being refetched due to observer-level error checks, and avoids TTL drift when staleTime or data updates occur.