Skip to content

Commit 8a61316

Browse files
committed
fix: make sure suspense callbacks get correct data
1 parent b124ca8 commit 8a61316

File tree

4 files changed

+74
-49
lines changed

4 files changed

+74
-49
lines changed

src/core/queriesObserver.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
1212
private result: QueryObserverResult[]
1313
private queries: QueryObserverOptions[]
1414
private observers: QueryObserver[]
15+
private observersMap: Record<string, QueryObserver>
1516

1617
constructor(client: QueryClient, queries?: QueryObserverOptions[]) {
1718
super()
1819

1920
this.client = client
20-
this.queries = queries || []
21+
this.queries = []
2122
this.result = []
2223
this.observers = []
23-
this.setQueries(this.queries)
24+
this.observersMap = {}
25+
26+
if (queries) {
27+
this.setQueries(queries)
28+
}
2429
}
2530

2631
protected onSubscribe(): void {
@@ -59,66 +64,62 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
5964
}
6065

6166
getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] {
62-
return queries.map((options, i) => {
63-
let observer: QueryObserver | undefined = this.observers[i]
64-
67+
return queries.map(options => {
6568
const defaultedOptions = this.client.defaultQueryObserverOptions(options)
66-
67-
if (
68-
!observer ||
69-
observer.getCurrentQuery().queryHash !== defaultedOptions.queryHash
70-
) {
71-
observer = this.observers.find(
72-
x => x.getCurrentQuery().queryHash === defaultedOptions.queryHash
73-
)
74-
}
75-
76-
if (!observer) {
77-
observer = new QueryObserver(this.client, defaultedOptions)
78-
}
79-
80-
return observer.getOptimisticResult(defaultedOptions)
69+
return this.getObserver(defaultedOptions).getOptimisticResult(
70+
defaultedOptions
71+
)
8172
})
8273
}
8374

75+
private getObserver(options: QueryObserverOptions): QueryObserver {
76+
const defaultedOptions = this.client.defaultQueryObserverOptions(options)
77+
return (
78+
this.observersMap[defaultedOptions.queryHash!] ||
79+
new QueryObserver(this.client, defaultedOptions)
80+
)
81+
}
82+
8483
private updateObservers(notifyOptions?: NotifyOptions): void {
8584
notifyManager.batch(() => {
8685
let hasIndexChange = false
8786

8887
const prevObservers = this.observers
89-
const newObservers = this.queries.map((options, i) => {
90-
let observer: QueryObserver | undefined = prevObservers[i]
88+
const prevOberversMap = this.observersMap
89+
90+
const newResult: QueryObserverResult[] = []
91+
const newObservers: QueryObserver[] = []
92+
const newObserversMap: Record<string, QueryObserver> = {}
9193

94+
this.queries.forEach((options, i) => {
9295
const defaultedOptions = this.client.defaultQueryObserverOptions(
9396
options
9497
)
98+
const queryHash = defaultedOptions.queryHash!
99+
const observer = this.getObserver(defaultedOptions)
95100

96-
if (
97-
!observer ||
98-
observer.getCurrentQuery().queryHash !== defaultedOptions.queryHash
99-
) {
100-
hasIndexChange = true
101-
observer = prevObservers.find(
102-
x => x.getCurrentQuery().queryHash === defaultedOptions.queryHash
103-
)
101+
if (prevOberversMap[queryHash]) {
102+
observer.setOptions(defaultedOptions, notifyOptions)
104103
}
105104

106-
if (observer) {
107-
observer.setOptions(defaultedOptions, notifyOptions)
108-
return observer
105+
if (observer !== prevObservers[i]) {
106+
hasIndexChange = true
109107
}
110108

111-
return new QueryObserver(this.client, defaultedOptions)
109+
newObservers.push(observer)
110+
newResult.push(observer.getCurrentResult())
111+
newObserversMap[queryHash] = observer
112112
})
113113

114114
if (prevObservers.length === newObservers.length && !hasIndexChange) {
115115
return
116116
}
117117

118118
this.observers = newObservers
119-
this.result = newObservers.map(observer => observer.getCurrentResult())
119+
this.observersMap = newObserversMap
120+
this.result = newResult
120121

121-
if (!this.listeners.length) {
122+
if (!this.hasListeners()) {
122123
return
123124
}
124125

src/core/queryObserver.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,21 @@ export class QueryObserver<
245245
return this.fetch(options)
246246
}
247247

248+
fetchOptimistic(
249+
options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
250+
): Promise<QueryObserverResult<TData, TError>> {
251+
const defaultedOptions = this.client.defaultQueryObserverOptions(options)
252+
253+
const query = this.client
254+
.getQueryCache()
255+
.build(
256+
this.client,
257+
defaultedOptions as QueryOptions<TQueryFnData, TError, TQueryData>
258+
)
259+
260+
return query.fetch().then(() => this.createResult(query, defaultedOptions))
261+
}
262+
248263
protected fetch(
249264
fetchOptions?: ObserverFetchOptions
250265
): Promise<QueryObserverResult<TData, TError>> {

src/react/tests/suspense.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,18 @@ describe("useQuery's in Suspense mode", () => {
133133
const successFn = jest.fn()
134134

135135
function Page() {
136-
useQuery([key], () => sleep(10), {
137-
suspense: true,
138-
onSuccess: successFn,
139-
})
136+
useQuery(
137+
[key],
138+
async () => {
139+
await sleep(10)
140+
return key
141+
},
142+
{
143+
suspense: true,
144+
select: () => 'selected',
145+
onSuccess: successFn,
146+
}
147+
)
140148

141149
return <>rendered</>
142150
}
@@ -151,6 +159,7 @@ describe("useQuery's in Suspense mode", () => {
151159
await waitFor(() => rendered.getByText('rendered'))
152160

153161
expect(successFn).toHaveBeenCalledTimes(1)
162+
expect(successFn).toHaveBeenCalledWith('selected')
154163
})
155164

156165
it('should call every onSuccess handler within a suspense boundary', async () => {

src/react/useBaseQuery.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
4141

4242
if (defaultedOptions.suspense) {
4343
// Always set stale time when using suspense to prevent
44-
// fetching again when directly re-mounting after suspense
44+
// fetching again when directly mounting after suspending
4545
if (typeof defaultedOptions.staleTime !== 'number') {
4646
defaultedOptions.staleTime = 1000
4747
}
@@ -65,9 +65,10 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
6565
React.useEffect(() => {
6666
mountedRef.current = true
6767

68+
errorResetBoundary.clearReset()
69+
6870
const unsubscribe = obsRef.current!.subscribe(
6971
notifyManager.batchCalls(() => {
70-
errorResetBoundary.clearReset()
7172
if (mountedRef.current) {
7273
forceUpdate(x => x + 1)
7374
}
@@ -91,10 +92,10 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
9192
}, [defaultedOptions])
9293

9394
// Handle suspense
94-
if (obsRef.current.options.suspense && result.isLoading) {
95-
throw queryClient
96-
.fetchQuery(defaultedOptions)
97-
.then(data => {
95+
if (defaultedOptions.suspense && result.isLoading) {
96+
throw obsRef.current
97+
.fetchOptimistic(defaultedOptions)
98+
.then(({ data }) => {
9899
defaultedOptions.onSuccess?.(data)
99100
defaultedOptions.onSettled?.(data, null)
100101
})
@@ -107,15 +108,14 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
107108

108109
// Handle error boundary
109110
if (
110-
(obsRef.current.options.suspense ||
111-
obsRef.current.options.useErrorBoundary) &&
111+
(defaultedOptions.suspense || defaultedOptions.useErrorBoundary) &&
112112
result.isError
113113
) {
114114
throw result.error
115115
}
116116

117117
// Handle result property usage tracking
118-
if (obsRef.current.options.notifyOnChangeProps === 'tracked') {
118+
if (defaultedOptions.notifyOnChangeProps === 'tracked') {
119119
result = obsRef.current.trackResult(result)
120120
}
121121

0 commit comments

Comments
 (0)