Skip to content

Commit 4758303

Browse files
authored
fix(query-core): move thenable-recreation into createResult (#8169)
* fix: move thenable-recreation into createResult `updateResult` will only be called after a fetch, but when we switch between caches without a fetch, we will only call `createResult`; this fix stops `data` from the queryResult and the `thenable` to go out-of-sync; it's backwards compatible because `updateResult` also invokes `createResult` * oops * test: I'm sick of this flaky test * chore: eslint reports an unused type assertion here
1 parent 4dfb0fc commit 4758303

File tree

4 files changed

+125
-27
lines changed

4 files changed

+125
-27
lines changed

packages/query-core/src/queryObserver.ts

+23-21
Original file line numberDiff line numberDiff line change
@@ -594,27 +594,7 @@ export class QueryObserver<
594594
promise: this.#currentThenable,
595595
}
596596

597-
return result as QueryObserverResult<TData, TError>
598-
}
599-
600-
updateResult(notifyOptions?: NotifyOptions): void {
601-
const prevResult = this.#currentResult as
602-
| QueryObserverResult<TData, TError>
603-
| undefined
604-
605-
const nextResult = this.createResult(this.#currentQuery, this.options)
606-
607-
this.#currentResultState = this.#currentQuery.state
608-
this.#currentResultOptions = this.options
609-
610-
if (this.#currentResultState.data !== undefined) {
611-
this.#lastQueryWithDefinedData = this.#currentQuery
612-
}
613-
614-
// Only notify and update result if something has changed
615-
if (shallowEqualObjects(nextResult, prevResult)) {
616-
return
617-
}
597+
const nextResult = result as QueryObserverResult<TData, TError>
618598

619599
if (this.options.experimental_prefetchInRender) {
620600
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
@@ -662,6 +642,28 @@ export class QueryObserver<
662642
}
663643
}
664644

645+
return nextResult
646+
}
647+
648+
updateResult(notifyOptions?: NotifyOptions): void {
649+
const prevResult = this.#currentResult as
650+
| QueryObserverResult<TData, TError>
651+
| undefined
652+
653+
const nextResult = this.createResult(this.#currentQuery, this.options)
654+
655+
this.#currentResultState = this.#currentQuery.state
656+
this.#currentResultOptions = this.options
657+
658+
if (this.#currentResultState.data !== undefined) {
659+
this.#lastQueryWithDefinedData = this.#currentQuery
660+
}
661+
662+
// Only notify and update result if something has changed
663+
if (shallowEqualObjects(nextResult, prevResult)) {
664+
return
665+
}
666+
665667
this.#currentResult = nextResult
666668

667669
// Determine which callbacks to trigger

packages/react-query/src/__tests__/useMutationState.test.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,19 @@ describe('useIsMutating', () => {
5959
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
6060
await sleep(10)
6161
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
62-
await waitFor(() => expect(isMutatingArray).toEqual([0, 1, 2, 1, 0]))
62+
63+
// we don't really care if this yields
64+
// [ +0, 1, 2, +0 ]
65+
// or
66+
// [ +0, 1, 2, 1, +0 ]
67+
// our batching strategy might yield different results
68+
69+
await waitFor(() => expect(isMutatingArray[0]).toEqual(0))
70+
await waitFor(() => expect(isMutatingArray[1]).toEqual(1))
71+
await waitFor(() => expect(isMutatingArray[2]).toEqual(2))
72+
await waitFor(() =>
73+
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0),
74+
)
6375
})
6476

6577
it('should filter correctly by mutationKey', async () => {

packages/react-query/src/__tests__/useQuery.test.tsx

+87
Original file line numberDiff line numberDiff line change
@@ -7385,5 +7385,92 @@ describe('useQuery', () => {
73857385
fireEvent.click(rendered.getByText('enable'))
73867386
await waitFor(() => rendered.getByText('test1'))
73877387
})
7388+
7389+
it('should show correct data when read from cache only (staleTime)', async () => {
7390+
const key = queryKey()
7391+
let suspenseRenderCount = 0
7392+
queryClient.setQueryData(key, 'initial')
7393+
7394+
function MyComponent(props: { promise: Promise<string> }) {
7395+
const data = React.use(props.promise)
7396+
7397+
return <>{data}</>
7398+
}
7399+
7400+
function Loading() {
7401+
suspenseRenderCount++
7402+
return <>loading..</>
7403+
}
7404+
function Page() {
7405+
const query = useQuery({
7406+
queryKey: key,
7407+
queryFn: async () => {
7408+
await sleep(1)
7409+
return 'test'
7410+
},
7411+
staleTime: Infinity,
7412+
})
7413+
7414+
return (
7415+
<React.Suspense fallback={<Loading />}>
7416+
<MyComponent promise={query.promise} />
7417+
</React.Suspense>
7418+
)
7419+
}
7420+
7421+
const rendered = renderWithClient(queryClient, <Page />)
7422+
await waitFor(() => rendered.getByText('initial'))
7423+
7424+
expect(suspenseRenderCount).toBe(0)
7425+
})
7426+
7427+
it('should show correct data when switching between cache entries without re-fetches', async () => {
7428+
const key = queryKey()
7429+
7430+
function MyComponent(props: { promise: Promise<string> }) {
7431+
const data = React.use(props.promise)
7432+
7433+
return <>{data}</>
7434+
}
7435+
7436+
function Loading() {
7437+
return <>loading..</>
7438+
}
7439+
function Page() {
7440+
const [count, setCount] = React.useState(0)
7441+
const query = useQuery({
7442+
queryKey: [key, count],
7443+
queryFn: async () => {
7444+
await sleep(10)
7445+
return 'test' + count
7446+
},
7447+
staleTime: Infinity,
7448+
})
7449+
7450+
return (
7451+
<div>
7452+
<React.Suspense fallback={<Loading />}>
7453+
<MyComponent promise={query.promise} />
7454+
</React.Suspense>
7455+
<button onClick={() => setCount(count + 1)}>inc</button>
7456+
<button onClick={() => setCount(count - 1)}>dec</button>
7457+
</div>
7458+
)
7459+
}
7460+
7461+
const rendered = renderWithClient(queryClient, <Page />)
7462+
await waitFor(() => rendered.getByText('loading..'))
7463+
await waitFor(() => rendered.getByText('test0'))
7464+
7465+
fireEvent.click(rendered.getByText('inc'))
7466+
await waitFor(() => rendered.getByText('loading..'))
7467+
7468+
await waitFor(() => rendered.getByText('test1'))
7469+
7470+
console.log('---------dec------------')
7471+
fireEvent.click(rendered.getByText('dec'))
7472+
7473+
await waitFor(() => rendered.getByText('test0'))
7474+
})
73887475
})
73897476
})

packages/vue-query/src/useMutationState.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,9 @@ export function useMutationState<TResult = MutationState>(
7171
): Readonly<Ref<Array<TResult>>> {
7272
const filters = computed(() => cloneDeepUnref(options.filters))
7373
const mutationCache = (queryClient || useQueryClient()).getMutationCache()
74-
const state = shallowRef(getResult(mutationCache, options)) as Ref<
75-
Array<TResult>
76-
>
74+
const state = shallowRef(getResult(mutationCache, options))
7775
const unsubscribe = mutationCache.subscribe(() => {
78-
const result = getResult(mutationCache, options)
79-
state.value = result
76+
state.value = getResult(mutationCache, options)
8077
})
8178

8279
watch(filters, () => {

0 commit comments

Comments
 (0)