Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react): allow using enabled when using useQuery().promise #8501

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
37 changes: 36 additions & 1 deletion 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 @@ -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 '..'

Expand Down Expand Up @@ -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<QueryObserverResult> = []

const success = pendingThenable<void>()

const unsubscribe = observer.subscribe((result) => {
results.push(result)
Comment on lines +1241 to +1251
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should just have a rejected promise when enabled is false instead?


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 @@ -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 (
<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 @@ -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(
() =>
Expand Down Expand Up @@ -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
Expand Down
Loading