Skip to content

Commit 985a423

Browse files
committed
feat: implement batch rendering
1 parent 00b9e96 commit 985a423

23 files changed

+176
-99
lines changed

rollup.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import commonJS from 'rollup-plugin-commonjs'
77
import visualizer from 'rollup-plugin-visualizer'
88
import replace from '@rollup/plugin-replace'
99

10-
const external = ['react']
10+
const external = ['react', 'react-dom']
1111
const hydrationExternal = [...external, 'react-query']
1212

1313
const globals = {
1414
react: 'React',
15+
'react-dom': 'ReactDOM',
1516
}
1617
const hydrationGlobals = {
1718
...globals,

src/core/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
export { queryCache, queryCaches, makeQueryCache } from './queryCache'
22
export { setFocusHandler } from './setFocusHandler'
33
export { setOnlineHandler } from './setOnlineHandler'
4-
export { CancelledError, isCancelledError, isError, setConsole } from './utils'
4+
export {
5+
CancelledError,
6+
isCancelledError,
7+
isError,
8+
setConsole,
9+
setBatchedUpdates,
10+
} from './utils'
511

612
// Types
713
export * from './types'

src/core/query.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,13 @@ export class Query<TResult, TError> {
131131
private dispatch(action: Action<TResult, TError>): void {
132132
this.state = queryReducer(this.state, action)
133133

134-
this.observers.forEach(observer => {
135-
observer.onQueryUpdate(action)
136-
})
134+
this.queryCache.batchNotifications(() => {
135+
this.observers.forEach(observer => {
136+
observer.onQueryUpdate(action)
137+
})
137138

138-
this.queryCache.notifyGlobalListeners(this)
139+
this.queryCache.notifyGlobalListeners(this)
140+
})
139141
}
140142

141143
private scheduleGc(): void {

src/core/queryCache.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
Updater,
33
deepIncludes,
44
functionalUpdate,
5+
getBatchedUpdates,
56
getQueryArgs,
67
isDocumentVisible,
7-
isPlainObject,
88
isOnline,
9+
isPlainObject,
910
isServer,
11+
scheduleMicrotask,
1012
} from './utils'
1113
import { getResolvedQueryConfig } from './config'
1214
import { Query } from './query'
@@ -66,6 +68,8 @@ type QueryCacheListener = (
6668
query?: Query<unknown, unknown>
6769
) => void
6870

71+
type NotifyCallback = () => void
72+
6973
// CLASS
7074

7175
export class QueryCache {
@@ -75,13 +79,44 @@ export class QueryCache {
7579
private globalListeners: QueryCacheListener[]
7680
private queries: QueryHashMap
7781
private queriesArray: Query<any, any>[]
82+
private notifyQueue: NotifyCallback[]
83+
private notifyTransactions: number
7884

7985
constructor(config?: QueryCacheConfig) {
8086
this.config = config || {}
8187
this.globalListeners = []
8288
this.queries = {}
8389
this.queriesArray = []
8490
this.isFetching = 0
91+
this.notifyQueue = []
92+
this.notifyTransactions = 0
93+
}
94+
95+
batchNotifications(callback: () => void): void {
96+
this.notifyTransactions++
97+
callback()
98+
this.notifyTransactions--
99+
if (!this.notifyTransactions && this.notifyQueue.length) {
100+
scheduleMicrotask(() => {
101+
const batchedUpdates = getBatchedUpdates()
102+
batchedUpdates(() => {
103+
this.notifyQueue.forEach(notify => {
104+
notify()
105+
})
106+
this.notifyQueue = []
107+
})
108+
})
109+
}
110+
}
111+
112+
scheduleNotification(notify: NotifyCallback): void {
113+
if (this.notifyTransactions) {
114+
this.notifyQueue.push(notify)
115+
} else {
116+
scheduleMicrotask(() => {
117+
notify()
118+
})
119+
}
85120
}
86121

87122
notifyGlobalListeners(query?: Query<any, any>) {
@@ -91,7 +126,9 @@ export class QueryCache {
91126
)
92127

93128
this.globalListeners.forEach(listener => {
94-
listener(this, query)
129+
this.scheduleNotification(() => {
130+
listener(this, query)
131+
})
95132
})
96133
}
97134

@@ -363,8 +400,10 @@ export function makeQueryCache(config?: QueryCacheConfig) {
363400
export function onVisibilityOrOnlineChange(type: 'focus' | 'online') {
364401
if (isDocumentVisible() && isOnline()) {
365402
queryCaches.forEach(queryCache => {
366-
queryCache.getQueries().forEach(query => {
367-
query.onInteraction(type)
403+
queryCache.batchNotifications(() => {
404+
queryCache.getQueries().forEach(query => {
405+
query.onInteraction(type)
406+
})
368407
})
369408
})
370409
}

src/core/queryObserver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ export class QueryObserver<TResult, TError> {
139139
}
140140

141141
private notify(): void {
142-
this.listener?.(this.currentResult)
142+
this.config.queryCache.scheduleNotification(() => {
143+
this.listener?.(this.currentResult)
144+
})
143145
}
144146

145147
private updateStaleTimeout(): void {

src/core/tests/queryCache.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
mockConsoleError,
66
mockNavigatorOnLine,
77
} from '../../react/tests/utils'
8-
import { makeQueryCache, queryCache as defaultQueryCache } from '..'
8+
import { makeQueryCache, queryCache as defaultQueryCache } from '../..'
99
import { isCancelledError, isError } from '../utils'
1010

1111
describe('queryCache', () => {
@@ -348,14 +348,15 @@ describe('queryCache', () => {
348348
expect(query.queryCache).toBe(queryCache)
349349
})
350350

351-
test('notifyGlobalListeners passes the same instance', () => {
351+
test('notifyGlobalListeners passes the same instance', async () => {
352352
const key = queryKey()
353353

354354
const queryCache = makeQueryCache()
355355
const subscriber = jest.fn()
356356
const unsubscribe = queryCache.subscribe(subscriber)
357357
const query = queryCache.buildQuery(key)
358358
query.setData('foo')
359+
await sleep(1)
359360
expect(subscriber).toHaveBeenCalledWith(queryCache, query)
360361

361362
unsubscribe()

src/core/tests/utils.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
deepIncludes,
55
isPlainObject,
66
} from '../utils'
7-
import { setConsole, queryCache } from '..'
7+
import { setConsole, queryCache } from '../..'
88
import { queryKey } from '../../react/tests/utils'
99

1010
describe('core/utils', () => {

src/core/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,34 @@ export function createSetHandler(fn: () => void) {
249249
removePreviousHandler = callback(fn)
250250
}
251251
}
252+
253+
/**
254+
* Schedules a microtask.
255+
* This can be useful to schedule state updates after rendering.
256+
*/
257+
export function scheduleMicrotask(callback: () => void): void {
258+
Promise.resolve()
259+
.then(callback)
260+
.catch(error =>
261+
setTimeout(() => {
262+
throw error
263+
})
264+
)
265+
}
266+
267+
type BatchUpdateFunction = (callback: () => void) => void
268+
269+
// Default to a dummy "batch" implementation that just runs the callback
270+
let batchedUpdates: BatchUpdateFunction = (callback: () => void) => {
271+
callback()
272+
}
273+
274+
// Allow injecting another batching function later
275+
export function setBatchedUpdates(fn: BatchUpdateFunction) {
276+
batchedUpdates = fn
277+
}
278+
279+
// Supply a getter just to skip dealing with ESM bindings
280+
export function getBatchedUpdates(): BatchUpdateFunction {
281+
return batchedUpdates
282+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
import { setBatchedUpdates } from './core'
2+
import { unstable_batchedUpdates } from './react/reactBatchedUpdates'
3+
setBatchedUpdates(unstable_batchedUpdates)
4+
15
export * from './core/index'
26
export * from './react/index'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// @ts-ignore
2+
import { unstable_batchedUpdates } from 'react-native'
3+
export { unstable_batchedUpdates }

0 commit comments

Comments
 (0)