Skip to content

Commit cdb2972

Browse files
committed
refactor: only batch react callbacks
1 parent 94c3e9e commit cdb2972

13 files changed

+95
-96
lines changed

src/core/mutationCache.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
6666
notify(mutation?: Mutation<any, any, any, any>) {
6767
notifyManager.batch(() => {
6868
this.listeners.forEach(listener => {
69-
notifyManager.schedule(() => {
70-
listener(mutation)
71-
})
69+
listener(mutation)
7270
})
7371
})
7472
}

src/core/mutationObserver.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,9 @@ export class MutationObserver<
118118
}
119119

120120
private notify() {
121-
const { currentResult } = this
122121
notifyManager.batch(() => {
123122
this.listeners.forEach(listener => {
124-
notifyManager.schedule(() => {
125-
listener(currentResult)
126-
})
123+
listener(this.currentResult)
127124
})
128125
})
129126
}

src/core/notifyManager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ class NotifyManager {
4949
}
5050
}
5151

52+
/**
53+
* All calls to the wrapped function will be batched.
54+
*/
55+
batchCalls<T extends Function>(callback: T): T {
56+
return ((...args: any[]) => {
57+
this.schedule(() => {
58+
callback(...args)
59+
})
60+
}) as any
61+
}
62+
5263
flush(): void {
5364
const queue = this.queue
5465
this.queue = []

src/core/queriesObserver.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,9 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
119119
}
120120

121121
private notify(): void {
122-
const { result } = this
123122
notifyManager.batch(() => {
124123
this.listeners.forEach(listener => {
125-
notifyManager.schedule(() => {
126-
listener(result)
127-
})
124+
listener(this.result)
128125
})
129126
})
130127
}

src/core/queryCache.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
112112
notify(query?: Query<any, any>) {
113113
notifyManager.batch(() => {
114114
this.listeners.forEach(listener => {
115-
notifyManager.schedule(() => {
116-
listener(query)
117-
})
115+
listener(query)
118116
})
119117
})
120118
}

src/core/queryObserver.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -462,47 +462,26 @@ export class QueryObserver<
462462
}
463463

464464
private notify(notifyOptions: NotifyOptions): void {
465-
const { currentResult, currentQuery, listeners } = this
466-
const { onSuccess, onSettled, onError } = this.options
467-
468465
notifyManager.batch(() => {
469466
// First trigger the configuration callbacks
470467
if (notifyOptions.onSuccess) {
471-
if (onSuccess) {
472-
notifyManager.schedule(() => {
473-
onSuccess(currentResult.data!)
474-
})
475-
}
476-
if (onSettled) {
477-
notifyManager.schedule(() => {
478-
onSettled(currentResult.data!, null)
479-
})
480-
}
468+
this.options.onSuccess?.(this.currentResult.data!)
469+
this.options.onSettled?.(this.currentResult.data!, null)
481470
} else if (notifyOptions.onError) {
482-
if (onError) {
483-
notifyManager.schedule(() => {
484-
onError(currentResult.error!)
485-
})
486-
}
487-
if (onSettled) {
488-
notifyManager.schedule(() => {
489-
onSettled(undefined, currentResult.error!)
490-
})
491-
}
471+
this.options.onError?.(this.currentResult.error!)
472+
this.options.onSettled?.(undefined, this.currentResult.error!)
492473
}
493474

494475
// Then trigger the listeners
495476
if (notifyOptions.listeners) {
496-
listeners.forEach(listener => {
497-
notifyManager.schedule(() => {
498-
listener(currentResult)
499-
})
477+
this.listeners.forEach(listener => {
478+
listener(this.currentResult)
500479
})
501480
}
502481

503482
// Then the cache listeners
504483
if (notifyOptions.cache) {
505-
this.client.getQueryCache().notify(currentQuery)
484+
this.client.getQueryCache().notify(this.currentQuery)
506485
}
507486
})
508487
}

src/core/subscribable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
type Listener = () => void
22

3-
export class Subscribable<TListener = Listener> {
3+
export class Subscribable<TListener extends Function = Listener> {
44
protected listeners: TListener[]
55

66
constructor() {

src/react/tests/useQuery.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,41 @@ describe('useQuery', () => {
340340
expect(onSuccess).toHaveBeenCalledWith('data')
341341
})
342342

343+
it('should not call onSuccess if a component has unmounted', async () => {
344+
const key = queryKey()
345+
const states: UseQueryResult<string>[] = []
346+
const onSuccess = jest.fn()
347+
348+
function Page() {
349+
const [show, setShow] = React.useState(true)
350+
351+
React.useEffect(() => {
352+
setShow(false)
353+
}, [setShow])
354+
355+
return show ? <Component /> : null
356+
}
357+
358+
function Component() {
359+
const state = useQuery(
360+
key,
361+
async () => {
362+
await sleep(10)
363+
return 'data'
364+
},
365+
{ onSuccess }
366+
)
367+
states.push(state)
368+
return null
369+
}
370+
371+
renderWithClient(queryClient, <Page />)
372+
373+
await sleep(50)
374+
expect(states.length).toBe(1)
375+
expect(onSuccess).toHaveBeenCalledTimes(0)
376+
})
377+
343378
it('should call onError after a query has been fetched with an error', async () => {
344379
const key = queryKey()
345380
const states: UseQueryResult<unknown>[] = []

src/react/useBaseQuery.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22

3-
import { useIsMounted } from './utils'
3+
import { notifyManager } from '../core/notifyManager'
44
import { QueryObserver } from '../core/queryObserver'
55
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
66
import { useQueryClient } from './QueryClientProvider'
@@ -11,10 +11,30 @@ export function useBaseQuery<TData, TError, TQueryFnData, TQueryData>(
1111
Observer: typeof QueryObserver
1212
) {
1313
const queryClient = useQueryClient()
14-
const isMounted = useIsMounted()
1514
const errorResetBoundary = useQueryErrorResetBoundary()
1615
const defaultedOptions = queryClient.defaultQueryObserverOptions(options)
1716

17+
// Batch calls to callbacks if not in suspense mode
18+
if (!defaultedOptions.suspense) {
19+
if (defaultedOptions.onError) {
20+
defaultedOptions.onError = notifyManager.batchCalls(
21+
defaultedOptions.onError
22+
)
23+
}
24+
25+
if (defaultedOptions.onSuccess) {
26+
defaultedOptions.onSuccess = notifyManager.batchCalls(
27+
defaultedOptions.onSuccess
28+
)
29+
}
30+
31+
if (defaultedOptions.onSettled) {
32+
defaultedOptions.onSettled = notifyManager.batchCalls(
33+
defaultedOptions.onSettled
34+
)
35+
}
36+
}
37+
1838
// Always set stale time when using suspense
1939
if (defaultedOptions.suspense && !defaultedOptions.staleTime) {
2040
defaultedOptions.staleTime = 2000
@@ -38,12 +58,8 @@ export function useBaseQuery<TData, TError, TQueryFnData, TQueryData>(
3858
// Subscribe to the observer
3959
React.useEffect(() => {
4060
errorResetBoundary.clearReset()
41-
return observer.subscribe(result => {
42-
if (isMounted()) {
43-
setCurrentResult(result)
44-
}
45-
})
46-
}, [isMounted, observer, errorResetBoundary])
61+
return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
62+
}, [observer, errorResetBoundary])
4763

4864
// Handle suspense
4965
if (observer.options.suspense || observer.options.useErrorBoundary) {

src/react/useIsFetching.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react'
22

3-
import { QueryKey } from '../core'
3+
import { notifyManager } from '../core/notifyManager'
4+
import { QueryKey } from '../core/types'
45
import { parseFilterArgs, QueryFilters } from '../core/utils'
56
import { useQueryClient } from './QueryClientProvider'
6-
import { useIsMounted } from './utils'
77

88
export function useIsFetching(filters?: QueryFilters): number
99
export function useIsFetching(
@@ -15,7 +15,6 @@ export function useIsFetching(
1515
arg2?: QueryFilters
1616
): number {
1717
const queryClient = useQueryClient()
18-
const isMounted = useIsMounted()
1918
const [filters] = parseFilterArgs(arg1, arg2)
2019
const [isFetching, setIsFetching] = React.useState(
2120
queryClient.isFetching(filters)
@@ -28,15 +27,15 @@ export function useIsFetching(
2827

2928
React.useEffect(
3029
() =>
31-
queryClient.getQueryCache().subscribe(() => {
32-
if (isMounted()) {
30+
queryClient.getQueryCache().subscribe(
31+
notifyManager.batchCalls(() => {
3332
const newIsFetching = queryClient.isFetching(filtersRef.current)
3433
if (isFetchingRef.current !== newIsFetching) {
3534
setIsFetching(newIsFetching)
3635
}
37-
}
38-
}),
39-
[queryClient, isMounted]
36+
})
37+
),
38+
[queryClient]
4039
)
4140

4241
return isFetching

0 commit comments

Comments
 (0)