Skip to content

Commit 2054f5b

Browse files
authored
fix(queryObserver): make sure that memoized select function only runs once per input (TanStack#3098)
we compute the result optimistically, and then one more time when calling setOptions. Both times run the select function because only setOptions will update the previous result and previous options. storing the "previous" selectFn separately on the observer whenever it was invoked (independent of the fact from where it was called) side-steps the issue
1 parent f9077bc commit 2054f5b

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

src/core/queryObserver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class QueryObserver<
6969
>
7070
private previousQueryResult?: QueryObserverResult<TData, TError>
7171
private previousSelectError: Error | null
72+
private previousSelectFn?: (data: TQueryData) => TData
7273
private staleTimeoutId?: number
7374
private refetchIntervalId?: number
7475
private currentRefetchInterval?: number | false
@@ -496,12 +497,13 @@ export class QueryObserver<
496497
if (
497498
prevResult &&
498499
state.data === prevResultState?.data &&
499-
options.select === prevResultOptions?.select &&
500+
options.select === this.previousSelectFn &&
500501
!this.previousSelectError
501502
) {
502503
data = prevResult.data
503504
} else {
504505
try {
506+
this.previousSelectFn = options.select
505507
data = options.select(state.data)
506508
if (options.structuralSharing !== false) {
507509
data = replaceEqualDeep(prevResult?.data, data)

src/react/tests/useQuery.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3951,6 +3951,53 @@ describe('useQuery', () => {
39513951
expect(placeholderFunctionRunCount).toEqual(1)
39523952
})
39533953

3954+
it('select should only run when dependencies change if memoized', async () => {
3955+
const key1 = queryKey()
3956+
3957+
let selectRun = 0
3958+
3959+
function Page() {
3960+
const [count, inc] = React.useReducer(prev => prev + 1, 2)
3961+
3962+
const state = useQuery(
3963+
key1,
3964+
async () => {
3965+
await sleep(10)
3966+
return 0
3967+
},
3968+
{
3969+
select: React.useCallback(
3970+
(data: number) => {
3971+
selectRun++
3972+
return `selected ${data + count}`
3973+
},
3974+
[count]
3975+
),
3976+
placeholderData: 99,
3977+
}
3978+
)
3979+
3980+
return (
3981+
<div>
3982+
<h2>Data: {state.data}</h2>
3983+
<button onClick={inc}>inc: {count}</button>
3984+
</div>
3985+
)
3986+
}
3987+
3988+
const rendered = renderWithClient(queryClient, <Page />)
3989+
await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2
3990+
expect(selectRun).toBe(1)
3991+
3992+
await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2
3993+
expect(selectRun).toBe(2)
3994+
3995+
rendered.getByRole('button', { name: /inc/i }).click()
3996+
3997+
await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3
3998+
expect(selectRun).toBe(3)
3999+
})
4000+
39544001
it('should cancel the query function when there are no more subscriptions', async () => {
39554002
const key = queryKey()
39564003
let cancelFn: jest.Mock = jest.fn()

0 commit comments

Comments
 (0)