Skip to content

Commit 7953639

Browse files
committed
fix(query-core): ensure combine re-executes after cache restoration with memoized combine
1 parent a1b1279 commit 7953639

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

packages/query-core/src/queriesObserver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ export class QueriesObserver<
123123
)
124124

125125
if (prevObservers.length === newObservers.length && !hasIndexChange) {
126+
const resultChanged = newResult.some((result, index) => {
127+
const prev = this.#result[index]
128+
return (
129+
!prev ||
130+
result.data !== prev.data ||
131+
result.isPending !== prev.isPending
132+
)
133+
})
134+
135+
if (resultChanged) {
136+
this.#result = newResult
137+
this.#notify()
138+
}
139+
126140
return
127141
}
128142

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2+
import { render, waitFor } from '@testing-library/react'
3+
import * as React from 'react'
4+
import { QueryClient, useQueries } from '@tanstack/react-query'
5+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
6+
import type {
7+
PersistedClient,
8+
Persister,
9+
} from '@tanstack/query-persist-client-core'
10+
import type { QueryObserverResult } from '@tanstack/react-query'
11+
12+
describe('useQueries with persist and memoized combine', () => {
13+
const storage: { [key: string]: string } = {}
14+
15+
beforeEach(() => {
16+
Object.defineProperty(window, 'localStorage', {
17+
value: {
18+
getItem: (key: string) => storage[key] || null,
19+
setItem: (key: string, value: string) => {
20+
storage[key] = value
21+
},
22+
removeItem: (key: string) => {
23+
delete storage[key]
24+
},
25+
clear: () => {
26+
Object.keys(storage).forEach((key) => delete storage[key])
27+
},
28+
},
29+
writable: true,
30+
})
31+
})
32+
33+
afterEach(() => {
34+
Object.keys(storage).forEach((key) => delete storage[key])
35+
})
36+
37+
it('should update UI when combine is memoized with persist', async () => {
38+
const queryClient = new QueryClient({
39+
defaultOptions: {
40+
queries: {
41+
staleTime: 30_000,
42+
gcTime: 1000 * 60 * 60 * 24,
43+
},
44+
},
45+
})
46+
47+
const persister: Persister = {
48+
persistClient: (client: PersistedClient) => {
49+
storage['REACT_QUERY_OFFLINE_CACHE'] = JSON.stringify(client)
50+
return Promise.resolve()
51+
},
52+
restoreClient: async () => {
53+
const stored = storage['REACT_QUERY_OFFLINE_CACHE']
54+
if (stored) {
55+
await new Promise((resolve) => setTimeout(resolve, 10))
56+
return JSON.parse(stored) as PersistedClient
57+
}
58+
return undefined
59+
},
60+
removeClient: () => {
61+
delete storage['REACT_QUERY_OFFLINE_CACHE']
62+
return Promise.resolve()
63+
},
64+
}
65+
66+
const persistedData: PersistedClient = {
67+
timestamp: Date.now(),
68+
buster: '',
69+
clientState: {
70+
mutations: [],
71+
queries: [1, 2, 3].map((id) => ({
72+
queryHash: `["post",${id}]`,
73+
queryKey: ['post', id],
74+
state: {
75+
data: id,
76+
dataUpdateCount: 1,
77+
dataUpdatedAt: Date.now() - 1000,
78+
error: null,
79+
errorUpdateCount: 0,
80+
errorUpdatedAt: 0,
81+
fetchFailureCount: 0,
82+
fetchFailureReason: null,
83+
fetchMeta: null,
84+
isInvalidated: false,
85+
status: 'success' as const,
86+
fetchStatus: 'idle' as const,
87+
},
88+
})),
89+
},
90+
}
91+
92+
storage['REACT_QUERY_OFFLINE_CACHE'] = JSON.stringify(persistedData)
93+
94+
function TestComponent() {
95+
const combinedQueries = useQueries({
96+
queries: [1, 2, 3].map((id) => ({
97+
queryKey: ['post', id],
98+
queryFn: () => Promise.resolve(id),
99+
staleTime: 30_000,
100+
})),
101+
combine: React.useCallback(
102+
(results: Array<QueryObserverResult<number, Error>>) => ({
103+
data: results.map((r) => r.data),
104+
isPending: results.some((r) => r.isPending),
105+
}),
106+
[],
107+
),
108+
})
109+
110+
return (
111+
<div>
112+
<div data-testid="pending">{String(combinedQueries.isPending)}</div>
113+
<div data-testid="data">
114+
{combinedQueries.data.filter((d) => d !== undefined).join(',')}
115+
</div>
116+
</div>
117+
)
118+
}
119+
120+
const { getByTestId } = render(
121+
<PersistQueryClientProvider
122+
client={queryClient}
123+
persistOptions={{ persister }}
124+
>
125+
<TestComponent />
126+
</PersistQueryClientProvider>,
127+
)
128+
129+
await waitFor(() => {
130+
expect(getByTestId('pending').textContent).toBe('false')
131+
expect(getByTestId('data').textContent).toBe('1,2,3')
132+
})
133+
})
134+
})

0 commit comments

Comments
 (0)