Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-subscribed-isfetching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-query': patch
---

Fix `isFetching` reporting `true` when `subscribed: false` and no fetch is actually running. `useBaseQuery` and `useQueries` no longer apply the `optimistic` fetching state when the observer will not subscribe, so `isFetching` and `fetchStatus` now stay consistent with the underlying query cache.
34 changes: 34 additions & 0 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5980,6 +5980,40 @@ describe('useQuery', () => {

expect(renders).toBe(1)
})

it('should not report isFetching=true when subscribed is false and no fetch is running', async () => {
const key = queryKey()
const queryFn = vi.fn(() => Promise.resolve('data'))
const states: Array<UseQueryResult<unknown>> = []

function Page() {
const state = useQuery({
queryKey: key,
queryFn,
subscribed: false,
})
states.push(state)
return (
<div>
<span>
fetchStatus: {state.fetchStatus} isFetching:{' '}
{String(state.isFetching)}
</span>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)

await vi.advanceTimersByTimeAsync(0)
rendered.getByText('fetchStatus: idle isFetching: false')

// No fetch is or will be running, so the observer must not advertise
// an optimistic fetching state.
expect(states.every((s) => s.isFetching === false)).toBe(true)
expect(states.every((s) => s.fetchStatus === 'idle')).toBe(true)
expect(queryFn).toHaveBeenCalledTimes(0)
})
})

it('should have status=error on mount when a query has failed', async () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,14 @@ export function useBaseQuery<
}

// Make sure results are optimistically set in fetching state before subscribing or updating options
// When `subscribed: false`, the observer will never subscribe, so no fetch will be triggered.
// In that case we must not flip the optimistic result into the fetching state, otherwise
// `isFetching` would be reported as `true` even though no fetch is or will be happening.
defaultedOptions._optimisticResults = isRestoring
? 'isRestoring'
: 'optimistic'
: options.subscribed === false
? undefined
: 'optimistic'

ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
Expand Down
11 changes: 8 additions & 3 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,21 +224,26 @@ export function useQueries<
const isRestoring = useIsRestoring()
const errorResetBoundary = useQueryErrorResetBoundary()

const subscribed = options.subscribed !== false
const defaultedQueries = React.useMemo(
() =>
queries.map((opts) => {
const defaultedOptions = client.defaultQueryOptions(
opts as QueryObserverOptions,
)

// Make sure the results are already in fetching state before subscribing or updating options
// Make sure the results are already in fetching state before subscribing or updating options.
// When `subscribed: false`, the observer will never subscribe, so no fetch will be triggered.
// Skip the optimistic fetching state in that case so `isFetching` does not lie.
defaultedOptions._optimisticResults = isRestoring
? 'isRestoring'
: 'optimistic'
: subscribed
? 'optimistic'
: undefined

return defaultedOptions
}),
[queries, client, isRestoring],
[queries, client, isRestoring, subscribed],
)

defaultedQueries.forEach((queryOptions) => {
Expand Down