Skip to content

Commit

Permalink
fix(query-core): move thenable-recreation into createResult (#8169)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TkDodo authored Oct 12, 2024
1 parent 4dfb0fc commit 4758303
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 27 deletions.
44 changes: 23 additions & 21 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,27 +594,7 @@ export class QueryObserver<
promise: this.#currentThenable,
}

return result as QueryObserverResult<TData, TError>
}

updateResult(notifyOptions?: NotifyOptions): void {
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined

const nextResult = this.createResult(this.#currentQuery, this.options)

this.#currentResultState = this.#currentQuery.state
this.#currentResultOptions = this.options

if (this.#currentResultState.data !== undefined) {
this.#lastQueryWithDefinedData = this.#currentQuery
}

// Only notify and update result if something has changed
if (shallowEqualObjects(nextResult, prevResult)) {
return
}
const nextResult = result as QueryObserverResult<TData, TError>

if (this.options.experimental_prefetchInRender) {
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
Expand Down Expand Up @@ -662,6 +642,28 @@ export class QueryObserver<
}
}

return nextResult
}

updateResult(notifyOptions?: NotifyOptions): void {
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined

const nextResult = this.createResult(this.#currentQuery, this.options)

this.#currentResultState = this.#currentQuery.state
this.#currentResultOptions = this.options

if (this.#currentResultState.data !== undefined) {
this.#lastQueryWithDefinedData = this.#currentQuery
}

// Only notify and update result if something has changed
if (shallowEqualObjects(nextResult, prevResult)) {
return
}

this.#currentResult = nextResult

// Determine which callbacks to trigger
Expand Down
14 changes: 13 additions & 1 deletion packages/react-query/src/__tests__/useMutationState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,19 @@ describe('useIsMutating', () => {
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
await sleep(10)
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
await waitFor(() => expect(isMutatingArray).toEqual([0, 1, 2, 1, 0]))

// we don't really care if this yields
// [ +0, 1, 2, +0 ]
// or
// [ +0, 1, 2, 1, +0 ]
// our batching strategy might yield different results

await waitFor(() => expect(isMutatingArray[0]).toEqual(0))
await waitFor(() => expect(isMutatingArray[1]).toEqual(1))
await waitFor(() => expect(isMutatingArray[2]).toEqual(2))
await waitFor(() =>
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0),
)
})

it('should filter correctly by mutationKey', async () => {
Expand Down
87 changes: 87 additions & 0 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7385,5 +7385,92 @@ describe('useQuery', () => {
fireEvent.click(rendered.getByText('enable'))
await waitFor(() => rendered.getByText('test1'))
})

it('should show correct data when read from cache only (staleTime)', async () => {
const key = queryKey()
let suspenseRenderCount = 0
queryClient.setQueryData(key, 'initial')

function MyComponent(props: { promise: Promise<string> }) {
const data = React.use(props.promise)

return <>{data}</>
}

function Loading() {
suspenseRenderCount++
return <>loading..</>
}
function Page() {
const query = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(1)
return 'test'
},
staleTime: Infinity,
})

return (
<React.Suspense fallback={<Loading />}>
<MyComponent promise={query.promise} />
</React.Suspense>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('initial'))

expect(suspenseRenderCount).toBe(0)
})

it('should show correct data when switching between cache entries without re-fetches', async () => {
const key = queryKey()

function MyComponent(props: { promise: Promise<string> }) {
const data = React.use(props.promise)

return <>{data}</>
}

function Loading() {
return <>loading..</>
}
function Page() {
const [count, setCount] = React.useState(0)
const query = useQuery({
queryKey: [key, count],
queryFn: async () => {
await sleep(10)
return 'test' + count
},
staleTime: Infinity,
})

return (
<div>
<React.Suspense fallback={<Loading />}>
<MyComponent promise={query.promise} />
</React.Suspense>
<button onClick={() => setCount(count + 1)}>inc</button>
<button onClick={() => setCount(count - 1)}>dec</button>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('loading..'))
await waitFor(() => rendered.getByText('test0'))

fireEvent.click(rendered.getByText('inc'))
await waitFor(() => rendered.getByText('loading..'))

await waitFor(() => rendered.getByText('test1'))

console.log('---------dec------------')
fireEvent.click(rendered.getByText('dec'))

await waitFor(() => rendered.getByText('test0'))
})
})
})
7 changes: 2 additions & 5 deletions packages/vue-query/src/useMutationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,9 @@ export function useMutationState<TResult = MutationState>(
): Readonly<Ref<Array<TResult>>> {
const filters = computed(() => cloneDeepUnref(options.filters))
const mutationCache = (queryClient || useQueryClient()).getMutationCache()
const state = shallowRef(getResult(mutationCache, options)) as Ref<
Array<TResult>
>
const state = shallowRef(getResult(mutationCache, options))
const unsubscribe = mutationCache.subscribe(() => {
const result = getResult(mutationCache, options)
state.value = result
state.value = getResult(mutationCache, options)
})

watch(filters, () => {
Expand Down

0 comments on commit 4758303

Please sign in to comment.