diff --git a/docs/src/pages/docs/comparison.md b/docs/src/pages/docs/comparison.md index e3c6b69280..2862105787 100644 --- a/docs/src/pages/docs/comparison.md +++ b/docs/src/pages/docs/comparison.md @@ -33,16 +33,17 @@ Feature/Capability Key: | Scroll Recovery | ✅ | ✅ | ✅ | | Cache Manipulation | ✅ | ✅ | ✅ | | Outdated Query Dismissal | ✅ | ✅ | ✅ | +| Render Optimization2 | ✅ | 🛑 | 🛑 | | Auto Garbage Collection | ✅ | 🛑 | 🛑 | | Mutation Hooks | ✅ | 🟡 | ✅ | | Prefetching APIs | ✅ | 🔶 | ✅ | | Query Cancellation | ✅ | 🛑 | 🛑 | -| Partial Query Matching2 | ✅ | 🛑 | 🛑 | +| Partial Query Matching3 | ✅ | 🛑 | 🛑 | | Stale While Revalidate | ✅ | ✅ | 🛑 | | Stale Time Configuration | ✅ | 🛑 | 🛑 | | Window Focus Refetching | ✅ | ✅ | 🛑 | | Network Status Refetching | ✅ | ✅ | ✅ | -| Automatic Refetch after Mutation3 | 🔶 | 🔶 | ✅ | +| Automatic Refetch after Mutation4 | 🔶 | 🔶 | ✅ | | Cache Dehydration/Rehydration | ✅ | 🛑 | ✅ | | React Suspense (Experimental) | ✅ | ✅ | 🛑 | @@ -50,9 +51,11 @@ Feature/Capability Key: > **1 Lagged / "Lazy" Queries** - React Query provides a way to continue to see an existing query's data while the next query loads (similar to the same UX that suspense will soon provide natively). This is extremely important when writing pagination UIs or infinite loading UIs where you do not want to show a hard loading state whenever a new query is requested. Other libraries do not have this capability and render a hard loading state for the new query (unless it has been prefetched), while the new query loads. -> **2 Partial query matching** - Because React Query uses deterministic query key serialization, this allows you to manipulate variable groups of queries without having to know each individual query-key that you want to match, eg. you can refetch every query that starts with `todos` in its key, regardless of variables, or you can target specific queries with (or without) variables or nested properties, and even use a filter function to only match queries that pass your specific conditions. +> **2 Render Optimization** - React Query has excellent rendering performance. It will only re-render your components when a query is updated. For example because it has new data, or to indicate it is fetching. React Query also batches updates together to make sure your application only re-renders once when multiple components are using the same query. If you are only interested in the `data` or `error` properties, you can reduce the number of renders even more by setting `notifyOnStatusChange` to `false`. -> **3 Automatic Refetch after Mutation** - For truly automatic refetching to happen after a mutation occurs, a schema is necessary (like the one graphQL provides) along with heuristics that help the library know how to identify individual entities and entities types in that schema. +> **3 Partial query matching** - Because React Query uses deterministic query key serialization, this allows you to manipulate variable groups of queries without having to know each individual query-key that you want to match, eg. you can refetch every query that starts with `todos` in its key, regardless of variables, or you can target specific queries with (or without) variables or nested properties, and even use a filter function to only match queries that pass your specific conditions. + +> **4 Automatic Refetch after Mutation** - For truly automatic refetching to happen after a mutation occurs, a schema is necessary (like the one graphQL provides) along with heuristics that help the library know how to identify individual entities and entities types in that schema. [swr]: https://github.com/vercel/swr [apollo]: https://github.com/apollographql/apollo-client diff --git a/rollup.config.js b/rollup.config.js index 9b1e4f1ca6..3b5badf04d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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, diff --git a/src/core/index.ts b/src/core/index.ts index 5c5b240814..421c02474c 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,7 +2,13 @@ export { getDefaultReactQueryConfig } from './config' 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' diff --git a/src/core/notifyManager.ts b/src/core/notifyManager.ts new file mode 100644 index 0000000000..6e0b79e289 --- /dev/null +++ b/src/core/notifyManager.ts @@ -0,0 +1,55 @@ +import { getBatchedUpdates, scheduleMicrotask } from './utils' + +// TYPES + +type NotifyCallback = () => void + +// CLASS + +export class NotifyManager { + private queue: NotifyCallback[] + private transactions: number + + constructor() { + this.queue = [] + this.transactions = 0 + } + + batch(callback: () => void): void { + this.transactions++ + callback() + this.transactions-- + if (!this.transactions) { + this.flush() + } + } + + schedule(notify: NotifyCallback): void { + if (this.transactions) { + this.queue.push(notify) + } else { + scheduleMicrotask(() => { + notify() + }) + } + } + + flush(): void { + const queue = this.queue + this.queue = [] + if (queue.length) { + scheduleMicrotask(() => { + const batchedUpdates = getBatchedUpdates() + batchedUpdates(() => { + queue.forEach(notify => { + notify() + }) + }) + }) + } + } +} + +// SINGLETON + +export const notifyManager = new NotifyManager() diff --git a/src/core/query.ts b/src/core/query.ts index ea4ed582db..3d83258706 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -23,6 +23,7 @@ import { } from './types' import type { QueryCache } from './queryCache' import { QueryObserver, UpdateListener } from './queryObserver' +import { notifyManager } from './notifyManager' // TYPES @@ -127,11 +128,13 @@ export class Query { private dispatch(action: Action): void { this.state = queryReducer(this.state, action) - this.observers.forEach(observer => { - observer.onQueryUpdate(action) - }) + notifyManager.batch(() => { + this.observers.forEach(observer => { + observer.onQueryUpdate(action) + }) - this.queryCache.notifyGlobalListeners(this) + this.queryCache.notifyGlobalListeners(this) + }) } private scheduleGc(): void { diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts index c4eb38ab65..856571b7f3 100644 --- a/src/core/queryCache.ts +++ b/src/core/queryCache.ts @@ -3,8 +3,8 @@ import { deepIncludes, getQueryArgs, isDocumentVisible, - isPlainObject, isOnline, + isPlainObject, isServer, } from './utils' import { getResolvedQueryConfig } from './config' @@ -18,6 +18,7 @@ import { TypedQueryFunctionArgs, ResolvedQueryConfig, } from './types' +import { notifyManager } from './notifyManager' // TYPES @@ -89,8 +90,12 @@ export class QueryCache { 0 ) - this.globalListeners.forEach(listener => { - listener(this, query) + notifyManager.batch(() => { + this.globalListeners.forEach(listener => { + notifyManager.schedule(() => { + listener(this, query) + }) + }) }) } @@ -194,17 +199,18 @@ export class QueryCache { options || {} try { - await Promise.all( - this.getQueries(predicate, options).map(query => { - const enabled = query.isEnabled() + const promises: Promise[] = [] + notifyManager.batch(() => { + 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 @@ -349,9 +355,11 @@ export function makeQueryCache(config?: QueryCacheConfig) { export function onVisibilityOrOnlineChange(type: 'focus' | 'online') { if (isDocumentVisible() && isOnline()) { - queryCaches.forEach(queryCache => { - queryCache.getQueries().forEach(query => { - query.onInteraction(type) + notifyManager.batch(() => { + queryCaches.forEach(queryCache => { + queryCache.getQueries().forEach(query => { + query.onInteraction(type) + }) }) }) } diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index d46c2a4ae9..6a402f1a76 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -5,6 +5,7 @@ import { isValidTimeout, noop, } from './utils' +import { notifyManager } from './notifyManager' import type { QueryResult, ResolvedQueryConfig } from './types' import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query' @@ -139,8 +140,13 @@ export class QueryObserver { } } - private notify(): void { - this.listener?.(this.currentResult) + private notify(global?: boolean): void { + notifyManager.schedule(() => { + this.listener?.(this.currentResult) + if (global) { + this.config.queryCache.notifyGlobalListeners(this.currentQuery) + } + }) } private updateStaleTimeout(): void { @@ -162,8 +168,7 @@ export class QueryObserver { if (!this.isStale) { this.isStale = true this.updateResult() - this.notify() - this.config.queryCache.notifyGlobalListeners(this.currentQuery) + this.notify(true) } }, timeout) } @@ -211,20 +216,19 @@ export class QueryObserver { } private updateResult(): void { - const { currentQuery, previousQueryResult, config } = this - const { state } = currentQuery + const { state } = this.currentQuery let { data, status, updatedAt } = state let isPreviousData = false // Keep previous data if needed if ( - config.keepPreviousData && + this.config.keepPreviousData && state.isInitialData && - previousQueryResult?.isSuccess + this.previousQueryResult?.isSuccess ) { - data = previousQueryResult.data - updatedAt = previousQueryResult.updatedAt - status = previousQueryResult.status + data = this.previousQueryResult.data + updatedAt = this.previousQueryResult.updatedAt + status = this.previousQueryResult.status isPreviousData = true } diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx index 5f56aa2887..48a181fcfe 100644 --- a/src/core/tests/queryCache.test.tsx +++ b/src/core/tests/queryCache.test.tsx @@ -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', () => { @@ -348,7 +348,7 @@ 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() @@ -356,6 +356,7 @@ describe('queryCache', () => { const unsubscribe = queryCache.subscribe(subscriber) const query = queryCache.buildQuery(key) query.setData('foo') + await sleep(1) expect(subscriber).toHaveBeenCalledWith(queryCache, query) unsubscribe() diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx index da7adeb673..1e8cd54225 100644 --- a/src/core/tests/utils.test.tsx +++ b/src/core/tests/utils.test.tsx @@ -1,5 +1,5 @@ import { replaceEqualDeep, deepIncludes, isPlainObject } from '../utils' -import { setConsole, queryCache } from '..' +import { setConsole, queryCache } from '../..' import { queryKey } from '../../react/tests/utils' describe('core/utils', () => { diff --git a/src/core/utils.ts b/src/core/utils.ts index 7875894a1c..ddbce0d6cc 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -250,3 +250,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 +} diff --git a/src/index.ts b/src/index.ts index 24c75db981..adfaa66a12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,6 @@ +import { setBatchedUpdates } from './core/index' +import { unstable_batchedUpdates } from './react/reactBatchedUpdates' +setBatchedUpdates(unstable_batchedUpdates) + export * from './core/index' export * from './react/index' diff --git a/src/react/reactBatchedUpdates.native.ts b/src/react/reactBatchedUpdates.native.ts new file mode 100644 index 0000000000..ae1adf81d5 --- /dev/null +++ b/src/react/reactBatchedUpdates.native.ts @@ -0,0 +1,3 @@ +// @ts-ignore +import { unstable_batchedUpdates } from 'react-native' +export { unstable_batchedUpdates } diff --git a/src/react/reactBatchedUpdates.ts b/src/react/reactBatchedUpdates.ts new file mode 100644 index 0000000000..baa3d01ee3 --- /dev/null +++ b/src/react/reactBatchedUpdates.ts @@ -0,0 +1,2 @@ +import ReactDOM from 'react-dom' +export const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates diff --git a/src/react/tests/ReactQueryCacheProvider.test.tsx b/src/react/tests/ReactQueryCacheProvider.test.tsx index 20e6c8e313..3eef92a29a 100644 --- a/src/react/tests/ReactQueryCacheProvider.test.tsx +++ b/src/react/tests/ReactQueryCacheProvider.test.tsx @@ -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 () => { diff --git a/src/react/tests/ReactQueryConfigProvider.test.tsx b/src/react/tests/ReactQueryConfigProvider.test.tsx index b74c6b6f40..72c1a398bf 100644 --- a/src/react/tests/ReactQueryConfigProvider.test.tsx +++ b/src/react/tests/ReactQueryConfigProvider.test.tsx @@ -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 diff --git a/src/react/tests/ssr.test.tsx b/src/react/tests/ssr.test.tsx index 5178d17241..f3201fdb49 100644 --- a/src/react/tests/ssr.test.tsx +++ b/src/react/tests/ssr.test.tsx @@ -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 diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx index 387fce907c..551de97f1f 100644 --- a/src/react/tests/suspense.test.tsx +++ b/src/react/tests/suspense.test.tsx @@ -3,12 +3,12 @@ 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, ReactQueryErrorResetBoundary, useErrorResetBoundary, -} from '../ReactQueryErrorResetBoundary' +} from '../..' describe("useQuery's in Suspense mode", () => { it('should not call the queryFn twice when used in Suspense mode', async () => { diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx index a85f536d70..dbe58087e8 100644 --- a/src/react/tests/useInfiniteQuery.test.tsx +++ b/src/react/tests/useInfiniteQuery.test.tsx @@ -2,8 +2,7 @@ import { render, waitFor, fireEvent } from '@testing-library/react' import * as React from 'react' import { sleep, queryKey, waitForMs, mockConsoleError } from './utils' -import { useInfiniteQuery, useQueryCache } from '..' -import { InfiniteQueryResult } from '../../core' +import { useInfiniteQuery, useQueryCache, InfiniteQueryResult } from '../..' interface Result { items: number[] diff --git a/src/react/tests/useIsFetching.test.tsx b/src/react/tests/useIsFetching.test.tsx index 96f562dd51..2e7fb0a905 100644 --- a/src/react/tests/useIsFetching.test.tsx +++ b/src/react/tests/useIsFetching.test.tsx @@ -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 diff --git a/src/react/tests/useMutation.test.tsx b/src/react/tests/useMutation.test.tsx index 8d1e48318e..b4fb38d24f 100644 --- a/src/react/tests/useMutation.test.tsx +++ b/src/react/tests/useMutation.test.tsx @@ -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', () => { diff --git a/src/react/tests/usePaginatedQuery.test.tsx b/src/react/tests/usePaginatedQuery.test.tsx index 09aa22a086..7e0064d6a5 100644 --- a/src/react/tests/usePaginatedQuery.test.tsx +++ b/src/react/tests/usePaginatedQuery.test.tsx @@ -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 () => { diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx index 3238399621..5b66996790 100644 --- a/src/react/tests/useQuery.test.tsx +++ b/src/react/tests/useQuery.test.tsx @@ -9,8 +9,7 @@ import { mockConsoleError, waitForMs, } from './utils' -import { useQuery } from '..' -import { queryCache, QueryResult } from '../../core' +import { useQuery, queryCache, QueryResult } from '../..' describe('useQuery', () => { it('should return the correct types', () => { @@ -905,13 +904,10 @@ describe('useQuery', () => { render() - await waitFor(() => - expect(states).toMatchObject([ - { isStale: true }, - { isStale: false }, - { isStale: true }, - ]) - ) + await waitFor(() => expect(states.length).toBe(3)) + expect(states[0]).toMatchObject({ isStale: true }) + expect(states[1]).toMatchObject({ isStale: false }) + expect(states[2]).toMatchObject({ isStale: true }) }) it('should notify query cache when a query becomes stale', async () => { @@ -1037,6 +1033,31 @@ describe('useQuery', () => { expect(queryCache.getQuery(key)!.config.queryFn).toBe(queryFn1) }) + it('should batch re-renders', async () => { + const key = queryKey() + + let renders = 0 + + const queryFn = async () => { + await sleep(10) + return 'data' + } + + function Page() { + useQuery(key, queryFn) + useQuery(key, queryFn) + renders++ + return null + } + + render() + + await waitForMs(20) + + // Should be 2 instead of 3 + expect(renders).toBe(2) + }) + // See https://github.com/tannerlinsley/react-query/issues/170 it('should start with status idle if enabled is false', async () => { const key1 = queryKey() diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts index b811824812..7aebcc9e08 100644 --- a/src/react/useBaseQuery.ts +++ b/src/react/useBaseQuery.ts @@ -1,6 +1,6 @@ import React from 'react' -import { useRerenderer } from './utils' +import { useIsMounted } from './utils' import { getResolvedQueryConfig } from '../core/config' import { QueryObserver } from '../core/queryObserver' import { QueryResultBase, QueryKey, QueryConfig } from '../core/types' @@ -12,8 +12,9 @@ export function useBaseQuery( queryKey: QueryKey, config?: QueryConfig ): QueryResultBase { + const [, rerender] = React.useReducer(c => c + 1, 0) + const isMounted = useIsMounted() const cache = useQueryCache() - const rerender = useRerenderer() const contextConfig = useContextConfig() const errorResetBoundary = useErrorResetBoundary() @@ -35,9 +36,11 @@ export function useBaseQuery( React.useEffect( () => observer.subscribe(() => { - rerender() + if (isMounted()) { + rerender() + } }), - [observer, rerender] + [isMounted, observer, rerender] ) // Update config diff --git a/src/react/useIsFetching.ts b/src/react/useIsFetching.ts index e8acfa3352..2f3658f0b0 100644 --- a/src/react/useIsFetching.ts +++ b/src/react/useIsFetching.ts @@ -1,19 +1,21 @@ import React from 'react' import { useQueryCache } from './ReactQueryCacheProvider' -import { useSafeState } from './utils' +import { useIsMounted } from './utils' export function useIsFetching(): number { + const isMounted = useIsMounted() const queryCache = useQueryCache() - - const [isFetching, setIsFetching] = useSafeState(queryCache.isFetching) + const [isFetching, setIsFetching] = React.useState(queryCache.isFetching) React.useEffect( () => queryCache.subscribe(() => { - setIsFetching(queryCache.isFetching) + if (isMounted()) { + setIsFetching(queryCache.isFetching) + } }), - [queryCache, setIsFetching] + [queryCache, setIsFetching, isMounted] ) return isFetching diff --git a/src/react/utils.ts b/src/react/utils.ts index 034b03f4cb..ff279c30d5 100644 --- a/src/react/utils.ts +++ b/src/react/utils.ts @@ -2,7 +2,7 @@ import React from 'react' import { isServer } from '../core/utils' -function useIsMounted(): () => boolean { +export function useIsMounted(): () => boolean { const mountedRef = React.useRef(false) const isMounted = React.useCallback(() => mountedRef.current, []) @@ -27,47 +27,3 @@ export function useMountedCallback(callback: T): T { [callback, isMounted] ) as any) as T } - -/** - * This hook is a safe useState version which schedules state updates in microtasks - * to prevent updating a component state while React is rendering different components - * or when the component is not mounted anymore. - */ -export function useSafeState( - initialState: S | (() => S) -): [S, React.Dispatch>] { - const isMounted = useIsMounted() - const [state, setState] = React.useState(initialState) - - const safeSetState = React.useCallback( - (value: React.SetStateAction) => { - scheduleMicrotask(() => { - if (isMounted()) { - setState(value) - } - }) - }, - [isMounted] - ) - - return [state, safeSetState] -} - -export function useRerenderer() { - const [, setState] = useSafeState({}) - return React.useCallback(() => setState({}), [setState]) -} - -/** - * Schedules a microtask. - * This can be useful to schedule state updates after rendering. - */ -function scheduleMicrotask(callback: () => void): void { - Promise.resolve() - .then(callback) - .catch(error => - setTimeout(() => { - throw error - }) - ) -}