Skip to content

Commit d7faf86

Browse files
authored
fix(queryObserver): memoized select functions should not return outdated values (TanStack#3140)
not all runs to createResult are saved on `this.currentResult` (optimistically created updates do not - we just create the result again later). With the fix from TanStack#3098, we made sure that memoized select does not run again in those cases by storing every run. However, that now means that we cannot rely on `prevResult.data` because it might still contain outdated data. The solution is to store a pair of previous selectFn + data and return the exact data that was computed from the previous select fn if we get the exact same function passed
1 parent b977cf4 commit d7faf86

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

src/core/queryObserver.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ export class QueryObserver<
6969
>
7070
private previousQueryResult?: QueryObserverResult<TData, TError>
7171
private previousSelectError: Error | null
72-
private previousSelectFn?: (data: TQueryData) => TData
72+
private previousSelect?: {
73+
fn: (data: TQueryData) => TData
74+
result: TData
75+
}
7376
private staleTimeoutId?: number
7477
private refetchIntervalId?: number
7578
private currentRefetchInterval?: number | false
@@ -497,14 +500,17 @@ export class QueryObserver<
497500
if (
498501
prevResult &&
499502
state.data === prevResultState?.data &&
500-
options.select === this.previousSelectFn &&
503+
options.select === this.previousSelect?.fn &&
501504
!this.previousSelectError
502505
) {
503-
data = prevResult.data
506+
data = this.previousSelect.result
504507
} else {
505508
try {
506-
this.previousSelectFn = options.select
507509
data = options.select(state.data)
510+
this.previousSelect = {
511+
fn: options.select,
512+
result: data,
513+
}
508514
if (options.structuralSharing !== false) {
509515
data = replaceEqualDeep(prevResult?.data, data)
510516
}

src/react/tests/useQuery.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3998,6 +3998,56 @@ describe('useQuery', () => {
39983998
expect(selectRun).toBe(3)
39993999
})
40004000

4001+
it('select should always return the correct state', async () => {
4002+
const key1 = queryKey()
4003+
4004+
function Page() {
4005+
const [count, inc] = React.useReducer(prev => prev + 1, 2)
4006+
const [forceValue, forceUpdate] = React.useReducer(prev => prev + 1, 1)
4007+
4008+
const state = useQuery(
4009+
key1,
4010+
async () => {
4011+
await sleep(10)
4012+
return 0
4013+
},
4014+
{
4015+
select: React.useCallback(
4016+
(data: number) => {
4017+
return `selected ${data + count}`
4018+
},
4019+
[count]
4020+
),
4021+
placeholderData: 99,
4022+
}
4023+
)
4024+
4025+
return (
4026+
<div>
4027+
<h2>Data: {state.data}</h2>
4028+
<h2>forceValue: {forceValue}</h2>
4029+
<button onClick={inc}>inc: {count}</button>
4030+
<button onClick={forceUpdate}>forceUpdate</button>
4031+
</div>
4032+
)
4033+
}
4034+
4035+
const rendered = renderWithClient(queryClient, <Page />)
4036+
await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2
4037+
4038+
await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2
4039+
4040+
rendered.getByRole('button', { name: /inc/i }).click()
4041+
4042+
await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3
4043+
4044+
rendered.getByRole('button', { name: /forceUpdate/i }).click()
4045+
4046+
await waitFor(() => rendered.getByText('forceValue: 2'))
4047+
// data should still be 3 after an independent re-render
4048+
await waitFor(() => rendered.getByText('Data: selected 3'))
4049+
})
4050+
40014051
it('should cancel the query function when there are no more subscriptions', async () => {
40024052
const key = queryKey()
40034053
let cancelFn: jest.Mock = jest.fn()

0 commit comments

Comments
 (0)