Skip to content

Commit f1187b9

Browse files
committed
[Fix-5538]: Assign observer's current Result when an optimistic reading occurs
1 parent 18c9f77 commit f1187b9

File tree

3 files changed

+106
-8
lines changed

3 files changed

+106
-8
lines changed

packages/query-core/src/queryObserver.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,28 @@ export class QueryObserver<
240240
): QueryObserverResult<TData, TError> {
241241
const query = this.client.getQueryCache().build(this.client, options)
242242

243-
return this.createResult(query, options)
243+
const result = this.createResult(query, options);
244+
245+
if (!options.keepPreviousData) {
246+
// this assigns the optimistic result to the current Observer
247+
// because if the query function changes, useQuery will be performing
248+
// an effect where it would fetch again.
249+
// When the fetch finishes, we perform a deep data cloning in order
250+
// to reuse objects references. This deep data clone is performed against
251+
// the `observer.currentResult.data` property
252+
// When QueryKey changes, we refresh the query and get new `optimistic`
253+
// result, while we leave the `observer.currentResult`, so when new data
254+
// arrives, it finds the old `observer.currentResult` which is related
255+
// to the old QueryKey. Which means that currentResult and selectData are
256+
// out of sync already.
257+
// To solve this, we move the cursor of the currentResult everytime
258+
// an observer reads an optimistic value.
259+
260+
// When keeping the previous data, the result doesn't change until new
261+
// data arrives.
262+
this.currentResult = result
263+
}
264+
return result
244265
}
245266

246267
getCurrentResult(): QueryObserverResult<TData, TError> {

packages/react-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ describe('PersistQueryClientProvider', () => {
325325
await waitFor(() => rendered.getByText('data: null'))
326326
await waitFor(() => rendered.getByText('data: hydrated'))
327327

328-
expect(states).toHaveLength(3)
328+
expect(states).toHaveLength(2)
329329

330330
expect(fetched).toBe(false)
331331

@@ -340,9 +340,6 @@ describe('PersistQueryClientProvider', () => {
340340
fetchStatus: 'idle',
341341
data: 'hydrated',
342342
})
343-
344-
// #5443 seems like we get an extra render now ...
345-
expect(states[1]).toStrictEqual(states[2])
346343
})
347344

348345
test('should call onSuccess after successful restoring', async () => {

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

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -914,17 +914,19 @@ describe('useQuery', () => {
914914
// required to make sure no additional renders are happening after data is successfully fetched for the second time
915915
await sleep(100)
916916

917-
expect(states.length).toBe(5)
917+
expect(states.length).toBe(4)
918918
// First load
919919
expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false })
920920
// First success
921921
expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true })
922922
// Remove
923923
expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false })
924924
// Hook state update
925-
expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false })
925+
// this update is now skipped ! because nothing changed
926+
// see github issue: https://github.com/TanStack/query/issues/5538
927+
// expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false })
926928
// Second success
927-
expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true })
929+
expect(states[3]).toMatchObject({ isLoading: false, isSuccess: true })
928930
})
929931

930932
it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => {
@@ -3650,6 +3652,7 @@ describe('useQuery', () => {
36503652
)
36513653
act(() => setPrefetched(true))
36523654
}
3655+
36533656
prefetch()
36543657
}, [])
36553658

@@ -5879,6 +5882,7 @@ describe('useQuery', () => {
58795882
</div>
58805883
)
58815884
}
5885+
58825886
const rendered = renderWithClient(queryClient, <Page />)
58835887
const fetchBtn = rendered.getByRole('button', { name: 'refetch' })
58845888
await waitFor(() => rendered.getByText('data: 1'))
@@ -5916,8 +5920,84 @@ describe('useQuery', () => {
59165920
</div>
59175921
)
59185922
}
5923+
59195924
const rendered = renderWithClient(queryClient, <Page />)
59205925
await waitFor(() => rendered.getByText('status: success'))
59215926
await waitFor(() => rendered.getByText('data: 1'))
59225927
})
5928+
it('should reuse same data object reference when queryKey changes back to some cached data', async () => {
5929+
jest.useFakeTimers()
5930+
const spy = jest.fn()
5931+
5932+
function fetchNumber(id: number) {
5933+
return new Promise((resolve) => {
5934+
setTimeout(
5935+
() =>
5936+
resolve({
5937+
numbers: {
5938+
current: {
5939+
id,
5940+
},
5941+
},
5942+
}),
5943+
1000,
5944+
)
5945+
})
5946+
}
5947+
function Test() {
5948+
const [id, setId] = React.useState(1)
5949+
5950+
const { data } = useQuery({
5951+
select: selector,
5952+
queryKey: ['user', id],
5953+
queryFn: () => fetchNumber(id),
5954+
})
5955+
5956+
React.useEffect(() => {
5957+
spy(data)
5958+
}, [data])
5959+
5960+
return (
5961+
<div>
5962+
<button name="1" onClick={() => setId(1)}>
5963+
1
5964+
</button>
5965+
<button name="2" onClick={() => setId(2)}>
5966+
2
5967+
</button>
5968+
<span>Rendered Id: {data?.id}</span>
5969+
</div>
5970+
)
5971+
}
5972+
5973+
function selector(data: any) {
5974+
return data.numbers.current
5975+
}
5976+
5977+
const rendered = renderWithClient(queryClient, <Test />)
5978+
expect(spy).toHaveBeenCalledTimes(1)
5979+
spy.mockClear()
5980+
jest.advanceTimersByTime(1000)
5981+
await waitFor(() => rendered.getByText('Rendered Id: 1'))
5982+
expect(spy).toHaveBeenCalledTimes(1)
5983+
spy.mockClear()
5984+
5985+
fireEvent.click(rendered.getByRole('button', { name: /2/ }))
5986+
jest.advanceTimersByTime(1000)
5987+
await waitFor(() => rendered.getByText('Rendered Id: 2'))
5988+
expect(spy).toHaveBeenCalledTimes(2) // called with undefined because id changed
5989+
spy.mockClear()
5990+
5991+
fireEvent.click(rendered.getByRole('button', { name: /1/ }))
5992+
jest.advanceTimersByTime(1000)
5993+
await waitFor(() => rendered.getByText('Rendered Id: 1'))
5994+
expect(spy).toHaveBeenCalledTimes(1)
5995+
spy.mockClear()
5996+
5997+
fireEvent.click(rendered.getByRole('button', { name: /2/ }))
5998+
jest.advanceTimersByTime(1000)
5999+
await waitFor(() => rendered.getByText('Rendered Id: 2'))
6000+
expect(spy).toHaveBeenCalledTimes(1)
6001+
spy.mockClear()
6002+
})
59236003
})

0 commit comments

Comments
 (0)