Skip to content
36 changes: 36 additions & 0 deletions packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/dom'
import {
afterEach,
beforeEach,
Expand All @@ -8,6 +9,7 @@ import {
vi,
} from 'vitest'
import { QueryObserver, focusManager } from '..'
import { pendingThenable } from '../thenable'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient, QueryObserverResult } from '..'

Expand Down Expand Up @@ -1271,4 +1273,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<QueryObserverResult> = []

const success = pendingThenable<void>()

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)
})
})
68 changes: 68 additions & 0 deletions packages/react-query/src/__tests__/useQuery.promise.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1382,4 +1382,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 (
<div>
<button onClick={() => setEnabled(true)}>enable</button>
<React.Suspense fallback={<Loading />}>
<MyComponent enabled={enabled} />
</React.Suspense>
</div>
)
}

const rendered = await renderStream.render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)

{
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)
})
})
15 changes: 11 additions & 4 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,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(
() =>
Expand Down Expand Up @@ -152,7 +150,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
Expand Down
Loading