From 2b871ea0dd536324bc0e62181f8fef589984f251 Mon Sep 17 00:00:00 2001 From: Niek Date: Wed, 9 Sep 2020 18:07:06 +0200 Subject: [PATCH] feat: implement batch rendering --- docs/src/pages/docs/comparison.md | 11 ++-- rollup.config.js | 3 +- src/core/index.ts | 8 ++- src/core/notifyManager.ts | 55 +++++++++++++++++++ src/core/query.ts | 11 ++-- src/core/queryCache.ts | 34 +++++++----- src/core/queryObserver.ts | 13 +++-- src/core/tests/queryCache.test.tsx | 5 +- src/core/tests/utils.test.tsx | 2 +- src/core/utils.ts | 31 +++++++++++ src/index.ts | 4 ++ src/react/reactBatchedUpdates.native.ts | 3 + src/react/reactBatchedUpdates.ts | 2 + .../tests/ReactQueryCacheProvider.test.tsx | 10 +++- .../tests/ReactQueryConfigProvider.test.tsx | 3 +- src/react/tests/ssr.test.tsx | 9 ++- src/react/tests/suspense.test.tsx | 6 +- src/react/tests/useInfiniteQuery.test.tsx | 3 +- src/react/tests/useIsFetching.test.tsx | 2 +- src/react/tests/useMutation.test.tsx | 2 +- src/react/tests/usePaginatedQuery.test.tsx | 3 +- src/react/tests/useQuery.test.tsx | 39 ++++++++++--- src/react/useBaseQuery.ts | 11 ++-- src/react/useIsFetching.ts | 12 ++-- src/react/utils.ts | 46 +--------------- 25 files changed, 220 insertions(+), 108 deletions(-) create mode 100644 src/core/notifyManager.ts create mode 100644 src/react/reactBatchedUpdates.native.ts create mode 100644 src/react/reactBatchedUpdates.ts diff --git a/docs/src/pages/docs/comparison.md b/docs/src/pages/docs/comparison.md index e3c6b69280b..2862105787d 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 9b1e4f1ca6e..3b5badf04de 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 5c5b2408144..421c02474cb 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 00000000000..6e0b79e289e --- /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 ea4ed582db1..3d832587065 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 c4eb38ab657..856571b7f3d 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 d46c2a4ae9e..7c722557ae6 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) } diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx index 5f56aa28874..48a181fcfe7 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 da7adeb6739..1e8cd542257 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 7875894a1ce..ddbce0d6cc6 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 24c75db981d..adfaa66a12a 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 00000000000..ae1adf81d54 --- /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 00000000000..baa3d01ee35 --- /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 20e6c8e313e..3eef92a29a3 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 b74c6b6f40e..72c1a398bf7 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 5178d172413..f3201fdb49a 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 387fce907c0..551de97f1fe 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 a85f536d70a..dbe58087e84 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 96f562dd519..2e7fb0a9051 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 8d1e48318ea..b4fb38d24f2 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 09aa22a086b..7e0064d6a5e 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 32383996218..5b669967909 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 b8118248125..7aebcc9e089 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 e8acfa3352a..2f3658f0b02 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 034b03f4cb5..ff279c30d5d 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 - }) - ) -}