diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index fa3890000d..d54da63309 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -1,3 +1,4 @@ +import { waitFor } from '@testing-library/dom' import { afterEach, beforeEach, @@ -7,8 +8,8 @@ import { test, vi, } from 'vitest' -import { waitFor } from '@testing-library/dom' import { QueryObserver, focusManager } from '..' +import { pendingThenable } from '../thenable' import { createQueryClient, queryKey, sleep } from './utils' import type { QueryClient, QueryObserverResult } from '..' @@ -1233,4 +1234,38 @@ describe('queryObserver', () => { unsubscribe() }) + + test('switching enabled state should reuse the same promise', async () => { + const key = queryKey() + + const observer = new QueryObserver(queryClient, { + queryKey: key, + enabled: false, + queryFn: () => 'data', + }) + const results: Array = [] + + const success = pendingThenable() + + const unsubscribe = observer.subscribe((result) => { + results.push(result) + + if (result.status === 'success') { + success.resolve() + } + }) + + observer.setOptions({ + queryKey: key, + queryFn: () => 'data', + enabled: true, + }) + + await success + + unsubscribe() + + const promises = new Set(results.map((result) => result.promise)) + expect(promises.size).toBe(1) + }) }) diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx index ec07d2d2b2..440e0119b5 100644 --- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.promise.test.tsx @@ -1377,4 +1377,72 @@ describe('useQuery().promise', () => { .observers.length, ).toBe(2) }) + + it('should handle enabled state changes with suspense', async () => { + const key = queryKey() + const renderStream = createRenderStream({ snapshotDOM: true }) + const queryFn = vi.fn(async () => { + await sleep(1) + return 'test' + }) + + function MyComponent(props: { enabled: boolean }) { + const query = useQuery({ + queryKey: key, + queryFn, + enabled: props.enabled, + staleTime: Infinity, + }) + + const data = React.use(query.promise) + return <>{data} + } + + function Loading() { + return <>loading.. + } + + function Page() { + const enabledState = React.useState(false) + const enabled = enabledState[0] + const setEnabled = enabledState[1] + + return ( +
+ + }> + + +
+ ) + } + + const rendered = await renderStream.render( + + + , + ) + + { + const result = await renderStream.takeRender() + result.withinDOM().getByText('loading..') + } + + expect(queryFn).toHaveBeenCalledTimes(0) + rendered.getByText('enable').click() + + { + const result = await renderStream.takeRender() + result.withinDOM().getByText('loading..') + } + + expect(queryFn).toHaveBeenCalledTimes(1) + + { + const result = await renderStream.takeRender() + result.withinDOM().getByText('test') + } + + expect(queryFn).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index bcbf700ef7..25de99ee3c 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -70,9 +70,7 @@ export function useBaseQuery< useClearResetErrorBoundary(errorResetBoundary) // this needs to be invoked before creating the Observer because that can create a cache entry - const isNewCacheEntry = !client - .getQueryCache() - .get(defaultedOptions.queryHash) + const cacheEntry = client.getQueryCache().get(defaultedOptions.queryHash) const [observer] = React.useState( () => @@ -143,7 +141,16 @@ export function useBaseQuery< !isServer && willFetch(result, isRestoring) ) { - const promise = isNewCacheEntry + // This fetching in the render should likely be done as part of the getOptimisticResult() considering https://github.com/TanStack/query/issues/8507 + const state = cacheEntry?.state + + const shouldFetch = + !state || + (state.data === undefined && + state.status === 'pending' && + state.fetchStatus === 'idle') + + const promise = shouldFetch ? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted fetchOptimistic(defaultedOptions, observer, errorResetBoundary) : // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in