Skip to content

Commit 70391e8

Browse files
authored
fix: include config callbacks in batch render (#1036)
1 parent 4361a88 commit 70391e8

File tree

3 files changed

+130
-31
lines changed

3 files changed

+130
-31
lines changed

src/core/queryCache.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ export class QueryCache {
133133
predicate?: QueryPredicate,
134134
options?: QueryPredicateOptions
135135
): Query<TResult, TError>[] {
136-
if (!options && (predicate === true || typeof predicate === 'undefined')) {
136+
const anyKey = predicate === true || typeof predicate === 'undefined'
137+
138+
if (anyKey && !options) {
137139
return this.queriesArray
138140
}
139141

@@ -142,20 +144,36 @@ export class QueryCache {
142144
if (typeof predicate === 'function') {
143145
predicateFn = predicate as QueryPredicateFn
144146
} else {
147+
const { exact, active, stale } = options || {}
145148
const resolvedConfig = this.getResolvedQueryConfig(predicate)
146149

147150
predicateFn = query => {
148-
if (
149-
options &&
150-
((options.exact && query.queryHash !== resolvedConfig.queryHash) ||
151-
(typeof options.active === 'boolean' &&
152-
query.isActive() !== options.active) ||
153-
(typeof options.stale === 'boolean' &&
154-
query.isStale() !== options.stale))
155-
) {
151+
// Check query key if needed
152+
if (!anyKey) {
153+
if (exact) {
154+
// Check if the query key matches exactly
155+
if (query.queryHash !== resolvedConfig.queryHash) {
156+
return false
157+
}
158+
} else {
159+
// Check if the query key matches partially
160+
if (!deepIncludes(query.queryKey, resolvedConfig.queryKey)) {
161+
return false
162+
}
163+
}
164+
}
165+
166+
// Check active state if needed
167+
if (typeof active === 'boolean' && query.isActive() !== active) {
156168
return false
157169
}
158-
return deepIncludes(query.queryKey, resolvedConfig.queryKey)
170+
171+
// Check stale state if needed
172+
if (typeof stale === 'boolean' && query.isStale() !== stale) {
173+
return false
174+
}
175+
176+
return true
159177
}
160178
}
161179

src/core/queryObserver.ts

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export type UpdateListener<TResult, TError> = (
1313
result: QueryResult<TResult, TError>
1414
) => void
1515

16+
interface NotifyOptions {
17+
globalListeners?: boolean
18+
listener?: boolean
19+
onError?: boolean
20+
onSuccess?: boolean
21+
}
22+
1623
export class QueryObserver<TResult, TError> {
1724
config: ResolvedQueryConfig<TResult, TError>
1825

@@ -150,13 +157,46 @@ export class QueryObserver<TResult, TError> {
150157
}
151158
}
152159

153-
private notify(global?: boolean): void {
160+
private notify(options: NotifyOptions): void {
161+
const { config, currentResult, currentQuery, listener } = this
162+
const { onSuccess, onSettled, onError } = config
163+
154164
notifyManager.batch(() => {
155-
notifyManager.schedule(() => {
156-
this.listener?.(this.currentResult)
157-
})
158-
if (global) {
159-
this.config.queryCache.notifyGlobalListeners(this.currentQuery)
165+
// First trigger the configuration callbacks
166+
if (options.onSuccess) {
167+
if (onSuccess) {
168+
notifyManager.schedule(() => {
169+
onSuccess(currentResult.data!)
170+
})
171+
}
172+
if (onSettled) {
173+
notifyManager.schedule(() => {
174+
onSettled(currentResult.data!, null)
175+
})
176+
}
177+
} else if (options.onError) {
178+
if (onError) {
179+
notifyManager.schedule(() => {
180+
onError(currentResult.error!)
181+
})
182+
}
183+
if (onSettled) {
184+
notifyManager.schedule(() => {
185+
onSettled(undefined, currentResult.error!)
186+
})
187+
}
188+
}
189+
190+
// Then trigger the listener
191+
if (options.listener && listener) {
192+
notifyManager.schedule(() => {
193+
listener(currentResult)
194+
})
195+
}
196+
197+
// Then the global listeners
198+
if (options.globalListeners) {
199+
config.queryCache.notifyGlobalListeners(currentQuery)
160200
}
161201
})
162202
}
@@ -180,7 +220,7 @@ export class QueryObserver<TResult, TError> {
180220
if (!this.isStale) {
181221
this.isStale = true
182222
this.updateResult()
183-
this.notify(true)
223+
this.notify({ listener: true, globalListeners: true })
184224
}
185225
}, timeout)
186226
}
@@ -327,28 +367,30 @@ export class QueryObserver<TResult, TError> {
327367
this.updateTimers()
328368
}
329369

330-
// Trigger callbacks on success or error
331-
if (type === 2) {
332-
config.onSuccess?.(currentResult.data!)
333-
config.onSettled?.(currentResult.data!, null)
334-
} else if (type === 3) {
335-
config.onError?.(currentResult.error!)
336-
config.onSettled?.(undefined, currentResult.error!)
337-
}
338-
339370
// Do not notify if the query was invalidated but the stale state did not changed
340371
if (type === 4 && currentResult.isStale === prevResult.isStale) {
341372
return
342373
}
343374

375+
// Determine which callbacks to trigger
376+
const notifyOptions: NotifyOptions = {}
377+
378+
if (type === 2) {
379+
notifyOptions.onSuccess = true
380+
} else if (type === 3) {
381+
notifyOptions.onError = true
382+
}
383+
344384
if (
345-
// Always notify on data or error change
385+
// Always notify if notifyOnStatusChange is set
386+
config.notifyOnStatusChange ||
387+
// Otherwise only notify on data or error change
346388
currentResult.data !== prevResult.data ||
347-
currentResult.error !== prevResult.error ||
348-
// Maybe notify on other changes
349-
config.notifyOnStatusChange
389+
currentResult.error !== prevResult.error
350390
) {
351-
this.notify()
391+
notifyOptions.listener = true
352392
}
393+
394+
this.notify(notifyOptions)
353395
}
354396
}

src/react/tests/useQuery.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,45 @@ describe('useQuery', () => {
11121112
expect(renders).toBe(2)
11131113
})
11141114

1115+
it('should batch re-renders including hook callbacks', async () => {
1116+
const key = queryKey()
1117+
1118+
let renders = 0
1119+
let renderedCount = 0
1120+
1121+
const queryFn = async () => {
1122+
await sleep(10)
1123+
return 'data'
1124+
}
1125+
1126+
function Page() {
1127+
const [count, setCount] = React.useState(0)
1128+
useQuery(key, queryFn, {
1129+
onSuccess: () => {
1130+
setCount(x => x + 1)
1131+
},
1132+
})
1133+
useQuery(key, queryFn, {
1134+
onSuccess: () => {
1135+
setCount(x => x + 1)
1136+
},
1137+
})
1138+
renders++
1139+
renderedCount = count
1140+
return null
1141+
}
1142+
1143+
render(<Page />)
1144+
1145+
await waitForMs(20)
1146+
1147+
// Should be 2 instead of 5
1148+
expect(renders).toBe(2)
1149+
1150+
// Both callbacks should have been executed
1151+
expect(renderedCount).toBe(2)
1152+
})
1153+
11151154
// See https://github.com/tannerlinsley/react-query/issues/170
11161155
it('should start with status idle if enabled is false', async () => {
11171156
const key1 = queryKey()

0 commit comments

Comments
 (0)