Skip to content

Commit 755702d

Browse files
authored
fix(queryObserver): fix data stability (TanStack#3183)
by making sure to memoize the structurally shared version of data rather than the raw one
1 parent d4c6798 commit 755702d

File tree

2 files changed

+49
-3
lines changed

2 files changed

+49
-3
lines changed

src/core/queryObserver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,13 @@ export class QueryObserver<
507507
} else {
508508
try {
509509
data = options.select(state.data)
510+
if (options.structuralSharing !== false) {
511+
data = replaceEqualDeep(prevResult?.data, data)
512+
}
510513
this.previousSelect = {
511514
fn: options.select,
512515
result: data,
513516
}
514-
if (options.structuralSharing !== false) {
515-
data = replaceEqualDeep(prevResult?.data, data)
516-
}
517517
this.previousSelectError = null
518518
} catch (selectError) {
519519
getLogger().error(selectError)

src/react/tests/useQuery.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4048,6 +4048,52 @@ describe('useQuery', () => {
40484048
await waitFor(() => rendered.getByText('Data: selected 3'))
40494049
})
40504050

4051+
it('select should structually share data', async () => {
4052+
const key1 = queryKey()
4053+
const states: Array<Array<number>> = []
4054+
4055+
function Page() {
4056+
const [forceValue, forceUpdate] = React.useReducer(prev => prev + 1, 1)
4057+
4058+
const state = useQuery(
4059+
key1,
4060+
async () => {
4061+
await sleep(10)
4062+
return [1, 2]
4063+
},
4064+
{
4065+
select: res => res.map(x => x + 1),
4066+
}
4067+
)
4068+
4069+
React.useEffect(() => {
4070+
if (state.data) {
4071+
states.push(state.data)
4072+
}
4073+
}, [state.data])
4074+
4075+
return (
4076+
<div>
4077+
<h2>Data: {JSON.stringify(state.data)}</h2>
4078+
<h2>forceValue: {forceValue}</h2>
4079+
<button onClick={forceUpdate}>forceUpdate</button>
4080+
</div>
4081+
)
4082+
}
4083+
4084+
const rendered = renderWithClient(queryClient, <Page />)
4085+
await waitFor(() => rendered.getByText('Data: [2,3]'))
4086+
expect(states).toHaveLength(1)
4087+
4088+
rendered.getByRole('button', { name: /forceUpdate/i }).click()
4089+
4090+
await waitFor(() => rendered.getByText('forceValue: 2'))
4091+
await waitFor(() => rendered.getByText('Data: [2,3]'))
4092+
4093+
// effect should not be triggered again due to structural sharing
4094+
expect(states).toHaveLength(1)
4095+
})
4096+
40514097
it('should cancel the query function when there are no more subscriptions', async () => {
40524098
const key = queryKey()
40534099
let cancelFn: jest.Mock = jest.fn()

0 commit comments

Comments
 (0)