diff --git a/.changeset/fix-subscribed-isfetching.md b/.changeset/fix-subscribed-isfetching.md new file mode 100644 index 00000000000..ff0643320a0 --- /dev/null +++ b/.changeset/fix-subscribed-isfetching.md @@ -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. diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 7504b9b4cb0..e8b30ba722b 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -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> = [] + + function Page() { + const state = useQuery({ + queryKey: key, + queryFn, + subscribed: false, + }) + states.push(state) + return ( +
+ + fetchStatus: {state.fetchStatus} isFetching:{' '} + {String(state.isFetching)} + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + 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 () => { diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index a88f7d40fbf..8cc1b4954c0 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -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) diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index de179837a5f..1e59605cd95 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -224,6 +224,7 @@ export function useQueries< const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() + const subscribed = options.subscribed !== false const defaultedQueries = React.useMemo( () => queries.map((opts) => { @@ -231,14 +232,18 @@ export function useQueries< 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) => {