Skip to content

Commit

Permalink
feat: implement batch rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
boschni committed Sep 10, 2020
1 parent 00b9e96 commit 3d88999
Show file tree
Hide file tree
Showing 23 changed files with 184 additions and 106 deletions.
3 changes: 2 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import commonJS from 'rollup-plugin-commonjs'
import visualizer from 'rollup-plugin-visualizer'
import replace from '@rollup/plugin-replace'

const external = ['react']
const external = ['react', 'react-dom']
const hydrationExternal = [...external, 'react-query']

const globals = {
react: 'React',
'react-dom': 'ReactDOM',
}
const hydrationGlobals = {
...globals,
Expand Down
8 changes: 7 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export { queryCache, queryCaches, makeQueryCache } from './queryCache'
export { setFocusHandler } from './setFocusHandler'
export { setOnlineHandler } from './setOnlineHandler'
export { CancelledError, isCancelledError, isError, setConsole } from './utils'
export {
CancelledError,
isCancelledError,
isError,
setConsole,
setBatchedUpdates,
} from './utils'

// Types
export * from './types'
Expand Down
10 changes: 6 additions & 4 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ export class Query<TResult, TError> {
private dispatch(action: Action<TResult, TError>): void {
this.state = queryReducer(this.state, action)

this.observers.forEach(observer => {
observer.onQueryUpdate(action)
})
this.queryCache.batchNotifications(() => {
this.observers.forEach(observer => {
observer.onQueryUpdate(action)
})

this.queryCache.notifyGlobalListeners(this)
this.queryCache.notifyGlobalListeners(this)
})
}

private scheduleGc(): void {
Expand Down
62 changes: 51 additions & 11 deletions src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
Updater,
deepIncludes,
functionalUpdate,
getBatchedUpdates,
getQueryArgs,
isDocumentVisible,
isPlainObject,
isOnline,
isPlainObject,
isServer,
scheduleMicrotask,
} from './utils'
import { getResolvedQueryConfig } from './config'
import { Query } from './query'
Expand Down Expand Up @@ -66,6 +68,8 @@ type QueryCacheListener = (
query?: Query<unknown, unknown>
) => void

type NotifyCallback = () => void

// CLASS

export class QueryCache {
Expand All @@ -75,13 +79,44 @@ export class QueryCache {
private globalListeners: QueryCacheListener[]
private queries: QueryHashMap
private queriesArray: Query<any, any>[]
private notifyQueue: NotifyCallback[]
private notifyTransactions: number

constructor(config?: QueryCacheConfig) {
this.config = config || {}
this.globalListeners = []
this.queries = {}
this.queriesArray = []
this.isFetching = 0
this.notifyQueue = []
this.notifyTransactions = 0
}

batchNotifications(callback: () => void): void {
this.notifyTransactions++
callback()
this.notifyTransactions--
if (!this.notifyTransactions && this.notifyQueue.length) {
scheduleMicrotask(() => {
const batchedUpdates = getBatchedUpdates()
batchedUpdates(() => {
this.notifyQueue.forEach(notify => {
notify()
})
this.notifyQueue = []
})
})
}
}

scheduleNotification(notify: NotifyCallback): void {
if (this.notifyTransactions) {
this.notifyQueue.push(notify)
} else {
scheduleMicrotask(() => {
notify()
})
}
}

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

this.globalListeners.forEach(listener => {
listener(this, query)
this.scheduleNotification(() => {
listener(this, query)
})
})
}

Expand Down Expand Up @@ -195,17 +232,18 @@ export class QueryCache {
options || {}

try {
await Promise.all(
this.getQueries(predicate, options).map(query => {
const enabled = query.isEnabled()
const promises: Promise<unknown>[] = []

this.batchNotifications(() => {
this.getQueries(predicate, options).forEach(query => {
const enabled = query.isEnabled()
if ((enabled && refetchActive) || (!enabled && refetchInactive)) {
return query.fetch()
promises.push(query.fetch())
}

return undefined
})
)
})

await Promise.all(promises)
} catch (err) {
if (throwOnError) {
throw err
Expand Down Expand Up @@ -363,8 +401,10 @@ export function makeQueryCache(config?: QueryCacheConfig) {
export function onVisibilityOrOnlineChange(type: 'focus' | 'online') {
if (isDocumentVisible() && isOnline()) {
queryCaches.forEach(queryCache => {
queryCache.getQueries().forEach(query => {
query.onInteraction(type)
queryCache.batchNotifications(() => {
queryCache.getQueries().forEach(query => {
query.onInteraction(type)
})
})
})
}
Expand Down
4 changes: 3 additions & 1 deletion src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export class QueryObserver<TResult, TError> {
}

private notify(): void {
this.listener?.(this.currentResult)
this.config.queryCache.scheduleNotification(() => {
this.listener?.(this.currentResult)
})
}

private updateStaleTimeout(): void {
Expand Down
5 changes: 3 additions & 2 deletions src/core/tests/queryCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
mockConsoleError,
mockNavigatorOnLine,
} from '../../react/tests/utils'
import { makeQueryCache, queryCache as defaultQueryCache } from '..'
import { makeQueryCache, queryCache as defaultQueryCache } from '../..'
import { isCancelledError, isError } from '../utils'

describe('queryCache', () => {
Expand Down Expand Up @@ -348,14 +348,15 @@ describe('queryCache', () => {
expect(query.queryCache).toBe(queryCache)
})

test('notifyGlobalListeners passes the same instance', () => {
test('notifyGlobalListeners passes the same instance', async () => {
const key = queryKey()

const queryCache = makeQueryCache()
const subscriber = jest.fn()
const unsubscribe = queryCache.subscribe(subscriber)
const query = queryCache.buildQuery(key)
query.setData('foo')
await sleep(1)
expect(subscriber).toHaveBeenCalledWith(queryCache, query)

unsubscribe()
Expand Down
2 changes: 1 addition & 1 deletion src/core/tests/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
deepIncludes,
isPlainObject,
} from '../utils'
import { setConsole, queryCache } from '..'
import { setConsole, queryCache } from '../..'
import { queryKey } from '../../react/tests/utils'

describe('core/utils', () => {
Expand Down
31 changes: 31 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,34 @@ export function createSetHandler(fn: () => void) {
removePreviousHandler = callback(fn)
}
}

/**
* Schedules a microtask.
* This can be useful to schedule state updates after rendering.
*/
export function scheduleMicrotask(callback: () => void): void {
Promise.resolve()
.then(callback)
.catch(error =>
setTimeout(() => {
throw error
})
)
}

type BatchUpdateFunction = (callback: () => void) => void

// Default to a dummy "batch" implementation that just runs the callback
let batchedUpdates: BatchUpdateFunction = (callback: () => void) => {
callback()
}

// Allow injecting another batching function later
export function setBatchedUpdates(fn: BatchUpdateFunction) {
batchedUpdates = fn
}

// Supply a getter just to skip dealing with ESM bindings
export function getBatchedUpdates(): BatchUpdateFunction {
return batchedUpdates
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
import { setBatchedUpdates } from './core'
import { unstable_batchedUpdates } from './react/reactBatchedUpdates'
setBatchedUpdates(unstable_batchedUpdates)

export * from './core/index'
export * from './react/index'
3 changes: 3 additions & 0 deletions src/react/reactBatchedUpdates.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-ignore
import { unstable_batchedUpdates } from 'react-native'
export { unstable_batchedUpdates }
2 changes: 2 additions & 0 deletions src/react/reactBatchedUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ReactDOM from 'react-dom'
export const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates
10 changes: 8 additions & 2 deletions src/react/tests/ReactQueryCacheProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import React, { useEffect } from 'react'
import { render, waitFor } from '@testing-library/react'

import { sleep, queryKey } from './utils'
import { ReactQueryCacheProvider, useQuery, useQueryCache } from '..'
import { makeQueryCache, queryCache, QueryCache } from '../../core'
import {
ReactQueryCacheProvider,
useQuery,
useQueryCache,
makeQueryCache,
queryCache,
QueryCache,
} from '../..'

describe('ReactQueryCacheProvider', () => {
test('when not used, falls back to global cache', async () => {
Expand Down
3 changes: 1 addition & 2 deletions src/react/tests/ReactQueryConfigProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import React, { useState } from 'react'
import { act, fireEvent, render, waitFor } from '@testing-library/react'

import { sleep, queryKey } from './utils'
import { ReactQueryConfigProvider, useQuery } from '..'
import { queryCache } from '../../core'
import { ReactQueryConfigProvider, useQuery, queryCache } from '../..'

describe('ReactQueryConfigProvider', () => {
// // See https://github.com/tannerlinsley/react-query/issues/105
Expand Down
9 changes: 7 additions & 2 deletions src/react/tests/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import React from 'react'
import { renderToString } from 'react-dom/server'

import { sleep, queryKey } from './utils'
import { usePaginatedQuery, ReactQueryCacheProvider, useQuery } from '..'
import { queryCache, makeQueryCache } from '../../core'
import {
usePaginatedQuery,
ReactQueryCacheProvider,
useQuery,
queryCache,
makeQueryCache,
} from '../..'

describe('Server Side Rendering', () => {
// A frozen cache does not cache any data. This is the default
Expand Down
3 changes: 1 addition & 2 deletions src/react/tests/suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { ErrorBoundary } from 'react-error-boundary'
import * as React from 'react'

import { sleep, queryKey, mockConsoleError } from './utils'
import { useQuery } from '..'
import { queryCache } from '../../core'
import { useQuery, queryCache } from '../..'

describe("useQuery's in Suspense mode", () => {
it('should not call the queryFn twice when used in Suspense mode', async () => {
Expand Down
3 changes: 1 addition & 2 deletions src/react/tests/useInfiniteQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { render, waitFor, fireEvent } from '@testing-library/react'
import * as React from 'react'

import { sleep, queryKey } from './utils'
import { useInfiniteQuery, useQueryCache } from '..'
import { InfiniteQueryResult } from '../../core'
import { useInfiniteQuery, useQueryCache, InfiniteQueryResult } from '../..'

interface Result {
items: number[]
Expand Down
2 changes: 1 addition & 1 deletion src/react/tests/useIsFetching.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'

import { sleep, queryKey, mockConsoleError } from './utils'
import { useQuery, useIsFetching } from '..'
import { useQuery, useIsFetching } from '../..'

describe('useIsFetching', () => {
// See https://github.com/tannerlinsley/react-query/issues/105
Expand Down
2 changes: 1 addition & 1 deletion src/react/tests/useMutation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render, fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'

import { useMutation } from '..'
import { useMutation } from '../..'
import { mockConsoleError } from './utils'

describe('useMutation', () => {
Expand Down
3 changes: 1 addition & 2 deletions src/react/tests/usePaginatedQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'

import { sleep, queryKey } from './utils'
import { usePaginatedQuery } from '..'
import { PaginatedQueryResult } from '../../core'
import { usePaginatedQuery, PaginatedQueryResult } from '../..'

describe('usePaginatedQuery', () => {
it('should return the correct states for a successful query', async () => {
Expand Down

0 comments on commit 3d88999

Please sign in to comment.