diff --git a/.github/workflows/test-and-publish.yml b/.github/workflows/test-and-publish.yml index 24d468633e..4a19418e13 100644 --- a/.github/workflows/test-and-publish.yml +++ b/.github/workflows/test-and-publish.yml @@ -13,11 +13,12 @@ on: jobs: test: - name: 'Node ${{ matrix.node }}' + name: 'Node ${{ matrix.node }}, React ${{ matrix.react }}' runs-on: ubuntu-latest strategy: matrix: node: [12, 14, 16] + react: [17, 18] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 @@ -26,8 +27,10 @@ jobs: - name: Install dependencies uses: bahmutov/npm-install@v1 - run: yarn test:ci + env: + REACTJS_VERSION: ${{ matrix.react }} - run: yarn test:size - if: matrix.node == '16' + if: matrix.node == '16' && matrix.react == '18' env: BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} - name: Upload coverage to Codecov diff --git a/jest.setup.js b/jest.setup.js index 64d6fe87ff..ac6e348860 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -6,3 +6,33 @@ import { notifyManager } from './src' notifyManager.setNotifyFunction(fn => { act(fn) }) + +jest.mock('react', () => { + const packages = { + 18: 'react', + 17: 'react-17', + } + const version = process.env.REACTJS_VERSION || '18' + + return jest.requireActual(packages[version]) +}) + +jest.mock('react-dom', () => { + const packages = { + 18: 'react-dom', + 17: 'react-dom-17', + } + const version = process.env.REACTJS_VERSION || '18' + + return jest.requireActual(packages[version]) +}) + +jest.mock('@testing-library/react', () => { + const packages = { + 18: '@testing-library/react', + 17: '@testing-library/react-17', + } + const version = process.env.REACTJS_VERSION || '18' + + return jest.requireActual(packages[version]) +}) diff --git a/package.json b/package.json index ab812e9dd1..4851911a28 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "scripts": { "test": "is-ci \"test:ci\" \"test:dev\"", "test:dev": "npm run test:types && npm run test:format && npm run test:eslint && npm run test:codemod && jest --watch", + "test:dev:17": "npm run test:types && npm run test:format && npm run test:eslint && npm run test:codemod && REACTJS_VERSION=17 jest --watch", "test:ci": "npm run test:types && npm run test:format && npm run test:eslint && npm run test:codemod && jest", "test:coverage": "yarn test:ci; open coverage/lcov-report/index.html", "test:format": "yarn prettier --check", @@ -69,11 +70,13 @@ ], "dependencies": { "@babel/runtime": "^7.5.5", + "@types/use-sync-external-store": "^0.0.3", "broadcast-channel": "^3.4.1", - "match-sorter": "^6.0.2" + "match-sorter": "^6.0.2", + "use-sync-external-store": "^1.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "react-dom": { @@ -92,12 +95,14 @@ "@babel/preset-typescript": "^7.16.7", "@rollup/plugin-replace": "^3.0.0", "@svgr/rollup": "^6.1.1", + "@testing-library/react": "^13.0.0", + "@testing-library/react-17": "npm:@testing-library/react@^12.1.4", "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^10.4.7", "@types/jest": "^26.0.4", "@types/jscodeshift": "^0.11.3", - "@types/react": "^16.9.41", - "@types/react-dom": "^16.9.8", + "@types/node": "^16.11.10", + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "babel-eslint": "^10.1.0", @@ -122,8 +127,10 @@ "jest": "^26.0.1", "jscodeshift": "^0.13.1", "prettier": "2.2.1", - "react": "^16.13.0", - "react-dom": "^16.13.1", + "react": "^18.0.0", + "react-17": "npm:react@^17.0.2", + "react-dom": "^18.0.0", + "react-dom-17": "npm:react-dom@^17.0.2", "react-error-boundary": "^2.2.2", "replace": "^1.2.0", "rimraf": "^3.0.2", diff --git a/rollup.config.js b/rollup.config.js index b54e73efaa..b47f2ccc39 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -44,6 +44,13 @@ const inputSrcs = [ const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'] const babelConfig = { extensions, runtimeHelpers: true } const resolveConfig = { extensions } +const commonJsConfig = { + namedExports: { + 'node_modules/use-sync-external-store/shim/index.js': [ + 'useSyncExternalStore', + ], + }, +} export default inputSrcs .map(([input, name, file]) => { @@ -61,7 +68,7 @@ export default inputSrcs plugins: [ resolve(resolveConfig), babel(babelConfig), - commonJS(), + commonJS(commonJsConfig), externalDeps(), ], }, @@ -83,7 +90,7 @@ export default inputSrcs }), resolve(resolveConfig), babel(babelConfig), - commonJS(), + commonJS(commonJsConfig), externalDeps(), terser(), size(), diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index fe82df5f2f..2b3a793ff8 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -570,15 +570,17 @@ export class QueryObserver< | QueryObserverResult | undefined - this.currentResult = this.createResult(this.currentQuery, this.options) + const nextResult = this.createResult(this.currentQuery, this.options) this.currentResultState = this.currentQuery.state this.currentResultOptions = this.options - // Only notify if something has changed - if (shallowEqualObjects(this.currentResult, prevResult)) { + // Only notify and update result if something has changed + if (shallowEqualObjects(nextResult, prevResult)) { return } + this.currentResult = nextResult + // Determine which callbacks to trigger const defaultNotifyOptions: NotifyOptions = { cache: true } diff --git a/src/core/subscribable.ts b/src/core/subscribable.ts index 2f574ff1b0..6b9e3a1a64 100644 --- a/src/core/subscribable.ts +++ b/src/core/subscribable.ts @@ -5,6 +5,7 @@ export class Subscribable { constructor() { this.listeners = [] + this.subscribe = this.subscribe.bind(this) } subscribe(listener: TListener): () => void { diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index dde571992c..df24be8319 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -1,4 +1,4 @@ -import { waitFor } from '@testing-library/react' +import { fireEvent, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' import React from 'react' @@ -379,7 +379,7 @@ describe('queryClient', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data')) - rendered.getByRole('button', { name: /setQueryData/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await waitFor(() => rendered.getByText('data: newData')) expect(onSuccess).toHaveBeenCalledTimes(1) @@ -409,7 +409,7 @@ describe('queryClient', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data')) - rendered.getByRole('button', { name: /setQueryData/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await waitFor(() => rendered.getByText('data: newData')) await waitFor(() => { expect(rendered.getByText('dataUpdatedAt: 100')).toBeInTheDocument() diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx index 4e4079063d..73c30071e0 100644 --- a/src/core/tests/utils.test.tsx +++ b/src/core/tests/utils.test.tsx @@ -5,9 +5,9 @@ import { parseMutationArgs, matchMutation, scheduleMicrotask, + sleep, } from '../utils' import { Mutation } from '../mutation' -import { waitFor } from '@testing-library/dom' import { createQueryClient } from '../../reactjs/tests/utils' describe('core/utils', () => { @@ -330,33 +330,13 @@ describe('core/utils', () => { }) describe('scheduleMicrotask', () => { - it('should throw an exception if the callback throw an error', async () => { - const error = new Error('error') - const callback = () => { - throw error - } - const errorSpy = jest.fn().mockImplementation(err => err) - jest.useFakeTimers() - const setTimeoutSpy = jest - .spyOn(globalThis, 'setTimeout') - .mockImplementation(function (handler: TimerHandler) { - try { - if (typeof handler === 'function') { - handler(errorSpy(error)) - } - } catch (err: any) { - expect(err.message).toEqual('error') - // Do no throw an uncaught exception that cannot be tested with - // this jest version - } - return 0 as any - }) + it('should defer execution of callback', async () => { + const callback = jest.fn() + scheduleMicrotask(callback) - jest.runAllTimers() - await waitFor(() => expect(setTimeoutSpy).toHaveBeenCalled()) - expect(errorSpy).toHaveBeenCalled() - setTimeoutSpy.mockRestore() - jest.useRealTimers() + expect(callback).not.toHaveBeenCalled() + await sleep(0) + expect(callback).toHaveBeenCalledTimes(1) }) }) }) diff --git a/src/core/utils.ts b/src/core/utils.ts index 14773db39b..ec5c7df778 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -404,14 +404,8 @@ export function sleep(timeout: number): Promise { * 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 - }) - ) +export function scheduleMicrotask(callback: () => void) { + sleep(0).then(callback) } export function getAbortController(): AbortController | undefined { diff --git a/src/devtools/devtools.tsx b/src/devtools/devtools.tsx index fc632fae90..34a1ac8f9b 100644 --- a/src/devtools/devtools.tsx +++ b/src/devtools/devtools.tsx @@ -1,14 +1,18 @@ import React from 'react' - +import { useSyncExternalStore } from 'use-sync-external-store/shim' +import { notifyManager } from '../core' import { Query, - ContextOptions, useQueryClient, onlineManager, + QueryCache, + QueryClient, + QueryKey as QueryKeyType, + ContextOptions, } from 'react-query' import { matchSorter } from 'match-sorter' import useLocalStorage from './useLocalStorage' -import { useIsMounted, useSafeState } from './utils' +import { useIsMounted } from './utils' import { Panel, @@ -109,8 +113,8 @@ export function ReactQueryDevtools({ 'reactQueryDevtoolsHeight', null ) - const [isResolvedOpen, setIsResolvedOpen] = useSafeState(false) - const [isResizing, setIsResizing] = useSafeState(false) + const [isResolvedOpen, setIsResolvedOpen] = React.useState(false) + const [isResizing, setIsResizing] = React.useState(false) const isMounted = useIsMounted() const handleDragStart = ( @@ -384,6 +388,21 @@ const sortFns: Record number> = { a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1, } +const useSubscribeToQueryCache = ( + queryCache: QueryCache, + getSnapshot: () => T +): T => { + return useSyncExternalStore( + React.useCallback( + onStoreChange => + queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), + [queryCache] + ), + getSnapshot, + getSnapshot + ) +} + export const ReactQueryDevtoolsPanel = React.forwardRef< HTMLDivElement, DevtoolsPanelOptions @@ -419,8 +438,9 @@ export const ReactQueryDevtoolsPanel = React.forwardRef< } }, [setSort, sortFn]) - const [unsortedQueries, setUnsortedQueries] = useSafeState( - Object.values(queryCache.findAll()) + const queriesCount = useSubscribeToQueryCache( + queryCache, + () => queryCache.getAll().length ) const [activeQueryHash, setActiveQueryHash] = useLocalStorage( @@ -429,7 +449,8 @@ export const ReactQueryDevtoolsPanel = React.forwardRef< ) const queries = React.useMemo(() => { - const sorted = [...unsortedQueries].sort(sortFn) + const unsortedQueries = queryCache.getAll() + const sorted = queriesCount > 0 ? [...unsortedQueries].sort(sortFn) : [] if (sortDesc) { sorted.reverse() @@ -442,41 +463,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef< return matchSorter(sorted, filter, { keys: ['queryHash'] }).filter( d => d.queryHash ) - }, [sortDesc, sortFn, unsortedQueries, filter]) - - const activeQuery = React.useMemo(() => { - return queries.find(query => query.queryHash === activeQueryHash) - }, [activeQueryHash, queries]) - - const hasFresh = queries.filter(q => getQueryStatusLabel(q) === 'fresh') - .length - const hasFetching = queries.filter(q => getQueryStatusLabel(q) === 'fetching') - .length - const hasPaused = queries.filter(q => getQueryStatusLabel(q) === 'paused') - .length - const hasStale = queries.filter(q => getQueryStatusLabel(q) === 'stale') - .length - const hasInactive = queries.filter(q => getQueryStatusLabel(q) === 'inactive') - .length - - React.useEffect(() => { - if (isOpen) { - const unsubscribe = queryCache.subscribe(() => { - setUnsortedQueries(Object.values(queryCache.getAll())) - }) - // re-subscribing after the panel is closed and re-opened won't trigger the callback, - // So we'll manually populate our state - setUnsortedQueries(Object.values(queryCache.getAll())) - - return unsubscribe - } - return undefined - }, [isOpen, sort, sortFn, sortDesc, setUnsortedQueries, queryCache]) - - const handleRefetch = () => { - const promise = activeQuery?.fetch() - promise?.catch(noop) - } + }, [sortDesc, sortFn, filter, queriesCount, queryCache]) const [isMockOffline, setMockOffline] = React.useState(false) @@ -558,50 +545,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef< flexDirection: 'column', }} > - - - fresh ({hasFresh}) - {' '} - - fetching ({hasFetching}) - {' '} - - paused ({hasPaused}) - {' '} - - stale ({hasStale}) - {' '} - - inactive ({hasInactive}) - - +
- {queries.map((query, i) => { + {queries.map(query => { return ( -
- setActiveQueryHash( - activeQueryHash === query.queryHash ? '' : query.queryHash - ) - } - style={{ - display: 'flex', - borderBottom: `solid 1px ${theme.grayAlt}`, - cursor: 'pointer', - background: - query === activeQuery - ? 'rgba(255,255,255,.1)' - : undefined, - }} - > -
- {query.getObserversCount()} -
- {query.isDisabled() ? ( -
- disabled -
- ) : null} - - {`${query.queryHash}`} - -
+ ) })}
- {activeQuery ? ( - -
- Query Details -
-
-
- -
-                    {JSON.stringify(activeQuery.queryKey, null, 2)}
-                  
-
- - {getQueryStatusLabel(activeQuery)} - -
-
- Observers: {activeQuery.getObserversCount()} -
-
- Last Updated:{' '} - - {new Date( - activeQuery.state.dataUpdatedAt - ).toLocaleTimeString()} - -
-
-
- Actions -
-
- {' '} - {' '} - {' '} - -
-
- Data Explorer -
-
- -
-
- Query Explorer -
-
- -
-
+ {activeQueryHash ? ( + ) : null} ) }) + +const ActiveQuery = ({ + queryCache, + activeQueryHash, + queryClient, +}: { + queryCache: QueryCache + activeQueryHash: string + queryClient: QueryClient +}) => { + const activeQuery = useSubscribeToQueryCache(queryCache, () => + queryCache.getAll().find(query => query.queryHash === activeQueryHash) + ) + + const activeQueryState = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().find(query => query.queryHash === activeQueryHash) + ?.state + ) + + const isStale = + useSubscribeToQueryCache(queryCache, () => + queryCache + .getAll() + .find(query => query.queryHash === activeQueryHash) + ?.isStale() + ) ?? false + + const observerCount = + useSubscribeToQueryCache(queryCache, () => + queryCache + .getAll() + .find(query => query.queryHash === activeQueryHash) + ?.getObserversCount() + ) ?? 0 + + const handleRefetch = () => { + const promise = activeQuery?.fetch() + promise?.catch(noop) + } + + if (!activeQuery || !activeQueryState) { + return null + } + + return ( + +
+ Query Details +
+
+
+ +
+              {JSON.stringify(activeQuery.queryKey, null, 2)}
+            
+
+ + {getQueryStatusLabel(activeQuery)} + +
+
+ Observers: {observerCount} +
+
+ Last Updated:{' '} + + {new Date(activeQueryState.dataUpdatedAt).toLocaleTimeString()} + +
+
+
+ Actions +
+
+ {' '} + {' '} + {' '} + +
+
+ Data Explorer +
+
+ +
+
+ Query Explorer +
+
+ +
+
+ ) +} + +const QueryStatusCount = ({ queryCache }: { queryCache: QueryCache }) => { + const hasFresh = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().filter(q => getQueryStatusLabel(q) === 'fresh').length + ) + const hasFetching = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().filter(q => getQueryStatusLabel(q) === 'fetching') + .length + ) + const hasPaused = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().filter(q => getQueryStatusLabel(q) === 'paused') + .length + ) + const hasStale = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().filter(q => getQueryStatusLabel(q) === 'stale').length + ) + const hasInactive = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().filter(q => getQueryStatusLabel(q) === 'inactive') + .length + ) + return ( + + + fresh ({hasFresh}) + {' '} + + fetching ({hasFetching}) + {' '} + + paused ({hasPaused}) + {' '} + + stale ({hasStale}) + {' '} + + inactive ({hasInactive}) + + + ) +} + +interface QueryRowProps { + queryKey: QueryKeyType + setActiveQueryHash: (hash: string) => void + activeQueryHash?: string + queryCache: QueryCache +} + +const QueryRow = ({ + queryKey, + setActiveQueryHash, + activeQueryHash, + queryCache, +}: QueryRowProps) => { + const queryHash = + useSubscribeToQueryCache( + queryCache, + () => queryCache.find(queryKey)?.queryHash + ) ?? '' + + const queryState = useSubscribeToQueryCache( + queryCache, + () => queryCache.find(queryKey)?.state + ) + + const isStale = + useSubscribeToQueryCache(queryCache, () => + queryCache.find(queryKey)?.isStale() + ) ?? false + + const isDisabled = + useSubscribeToQueryCache(queryCache, () => + queryCache.find(queryKey)?.isDisabled() + ) ?? false + + const observerCount = + useSubscribeToQueryCache(queryCache, () => + queryCache.find(queryKey)?.getObserversCount() + ) ?? 0 + + if (!queryState) { + return null + } + + return ( +
+ setActiveQueryHash(activeQueryHash === queryHash ? '' : queryHash) + } + style={{ + display: 'flex', + borderBottom: `solid 1px ${theme.grayAlt}`, + cursor: 'pointer', + background: + queryHash === activeQueryHash ? 'rgba(255,255,255,.1)' : undefined, + }} + > +
+ {observerCount} +
+ {isDisabled ? ( +
+ disabled +
+ ) : null} + + {`${queryHash}`} + +
+ ) +} diff --git a/src/devtools/tests/devtools.test.tsx b/src/devtools/tests/devtools.test.tsx index 30f2ac5263..ad245e675c 100644 --- a/src/devtools/tests/devtools.test.tsx +++ b/src/devtools/tests/devtools.test.tsx @@ -1,12 +1,8 @@ import React from 'react' + +import { fireEvent, screen, waitFor, act } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' -import { - fireEvent, - screen, - waitFor, - act, - waitForElementToBeRemoved, -} from '@testing-library/react' + import '@testing-library/jest-dom' import { useQuery, QueryClient } from '../..' import { @@ -31,6 +27,9 @@ Object.defineProperty(window, 'matchMedia', { }) describe('ReactQueryDevtools', () => { + beforeEach(() => { + localStorage.removeItem('reactQueryDevtoolsOpen') + }) it('should be able to open and close devtools', async () => { const { queryClient } = createQueryClient() const onCloseClick = jest.fn() @@ -63,10 +62,6 @@ describe('ReactQueryDevtools', () => { screen.getByRole('button', { name: /open react query devtools/i }) ) - await waitForElementToBeRemoved(() => - screen.queryByRole('button', { name: /open react query devtools/i }) - ) - expect(onToggleClick).toHaveBeenCalledTimes(1) fireEvent.click( @@ -229,11 +224,11 @@ describe('ReactQueryDevtools', () => { await screen.findByText(getByTextContent(`1${currentQuery?.queryHash}`)) - fireEvent.click( - screen.getByRole('button', { - name: `Open query details for ${currentQuery?.queryHash}`, - }) - ) + const queryButton = await screen.findByRole('button', { + name: `Open query details for ${currentQuery?.queryHash}`, + }) + + fireEvent.click(queryButton) await screen.findByText(/query details/i) }) @@ -320,11 +315,11 @@ describe('ReactQueryDevtools', () => { await screen.findByText(/disabled/i) - await act(async () => { - fireEvent.click(await screen.findByText(/enable query/i)) - }) + fireEvent.click(screen.getByRole('button', { name: /enable query/i })) - expect(screen.queryByText(/disabled/i)).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByText(/disabled/i)).not.toBeInTheDocument() + }) }) it('should not show a disabled label for inactive queries', async () => { @@ -357,11 +352,11 @@ describe('ReactQueryDevtools', () => { await screen.findByText(/disabled/i) - await act(async () => { - fireEvent.click(await screen.findByText(/hide query/i)) - }) + fireEvent.click(screen.getByRole('button', { name: /hide query/i })) - expect(screen.queryByText(/disabled/i)).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByText(/disabled/i)).not.toBeInTheDocument() + }) }) it('should simulate offline mode', async () => { @@ -383,28 +378,36 @@ describe('ReactQueryDevtools', () => { ) } - const rendered = renderWithClient(queryClient, , { + renderWithClient(queryClient, , { initialIsOpen: true, }) - await rendered.findByRole('heading', { name: /test/i }) + await screen.findByRole('heading', { name: /test/i }) - rendered.getByRole('button', { name: /mock offline behavior/i }).click() + fireEvent.click( + screen.getByRole('button', { name: /mock offline behavior/i }) + ) - rendered - .getByRole('button', { name: 'Open query details for ["key"]' }) - .click() + const queryButton = await screen.findByRole('button', { + name: 'Open query details for ["key"]', + }) + fireEvent.click(queryButton) - rendered.getByRole('button', { name: /refetch/i }).click() + const refetchButton = await screen.findByRole('button', { + name: /refetch/i, + }) + fireEvent.click(refetchButton) await waitFor(() => { - expect(rendered.getByText('test, paused')).toBeInTheDocument() + expect(screen.getByText('test, paused')).toBeInTheDocument() }) - rendered.getByRole('button', { name: /restore offline mock/i }).click() + fireEvent.click( + screen.getByRole('button', { name: /restore offline mock/i }) + ) await waitFor(() => { - expect(rendered.getByText('test, idle')).toBeInTheDocument() + expect(screen.getByText('test, idle')).toBeInTheDocument() }) expect(count).toBe(2) @@ -419,18 +422,24 @@ describe('ReactQueryDevtools', () => { return 'query-1-result' }) - const query2Result = useQuery(['query-2'], async () => { - await sleep(60) - return 'query-2-result' - }) - const query3Result = useQuery( ['query-3'], async () => { - await sleep(40) + await sleep(10) return 'query-3-result' }, - { staleTime: Infinity } + { staleTime: Infinity, enabled: typeof query1Result.data === 'string' } + ) + + const query2Result = useQuery( + ['query-2'], + async () => { + await sleep(10) + return 'query-2-result' + }, + { + enabled: typeof query3Result.data === 'string', + } ) return ( @@ -470,7 +479,7 @@ describe('ReactQueryDevtools', () => { expect(queries[2]?.textContent).toEqual(query3Hash) // Wait for the queries to be resolved - await sleep(70) + await screen.findByText(/query-1-result query-2-result query-3-result/i) // When sorted by the last updated date the queries are sorted by the time // they were updated and since the query-2 takes longest time to complete diff --git a/src/devtools/tests/utils.tsx b/src/devtools/tests/utils.tsx index 0c0a36b166..2761faf4cb 100644 --- a/src/devtools/tests/utils.tsx +++ b/src/devtools/tests/utils.tsx @@ -1,3 +1,4 @@ +import { MatcherFunction } from '@testing-library/dom/types/matches' import { render } from '@testing-library/react' import React from 'react' import { ReactQueryDevtools } from '../' @@ -41,11 +42,14 @@ export function sleep(timeout: number): Promise { * @param textToMatch The string that needs to be matched * @reference https://stackoverflow.com/a/56859650/8252081 */ -export const getByTextContent = (textToMatch: string) => ( - _content: string, - node: HTMLElement -): boolean => { - const hasText = (currentNode: HTMLElement) => +export const getByTextContent = (textToMatch: string): MatcherFunction => ( + _content, + node +) => { + if (!node) { + return false + } + const hasText = (currentNode: Element) => currentNode.textContent === textToMatch const nodeHasText = hasText(node) const childrenDontHaveText = Array.from(node.children).every( diff --git a/src/devtools/utils.ts b/src/devtools/utils.ts index 735e4fa452..56daf9201c 100644 --- a/src/devtools/utils.ts +++ b/src/devtools/utils.ts @@ -25,14 +25,24 @@ type StyledComponent = T extends 'button' ? React.HTMLAttributes : never -export function getQueryStatusColor(query: Query, theme: Theme) { - return query.state.fetchStatus === 'fetching' +export function getQueryStatusColor({ + queryState, + observerCount, + isStale, + theme, +}: { + queryState: Query['state'] + observerCount: number + isStale: boolean + theme: Theme +}) { + return queryState.fetchStatus === 'fetching' ? theme.active - : !query.getObserversCount() + : !observerCount ? theme.gray - : query.state.fetchStatus === 'paused' + : queryState.fetchStatus === 'paused' ? theme.paused - : query.isStale() + : isStale ? theme.warning : theme.success } @@ -104,29 +114,6 @@ export function useIsMounted() { return isMounted } -/** - * 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: T): [T, (value: T) => void] { - const isMounted = useIsMounted() - const [state, setState] = React.useState(initialState) - - const safeSetState = React.useCallback( - (value: T) => { - scheduleMicrotask(() => { - if (isMounted()) { - setState(value) - } - }) - }, - [isMounted] - ) - - return [state, safeSetState] -} - /** * Displays a string regardless the type of the data * @param {unknown} value Value to be stringified @@ -137,17 +124,3 @@ export const displayValue = (value: unknown) => { return JSON.stringify(newValue, name) } - -/** - * Schedules a microtask. - * This can be useful to schedule state updates after rendering. - */ -function scheduleMicrotask(callback: () => void) { - Promise.resolve() - .then(callback) - .catch(error => - setTimeout(() => { - throw error - }) - ) -} diff --git a/src/persistQueryClient/tests/PersistQueryClientProvider.test.tsx b/src/persistQueryClient/tests/PersistQueryClientProvider.test.tsx index 38cab6b4fb..537c519620 100644 --- a/src/persistQueryClient/tests/PersistQueryClientProvider.test.tsx +++ b/src/persistQueryClient/tests/PersistQueryClientProvider.test.tsx @@ -318,7 +318,7 @@ describe('PersistQueryClientProvider', () => { await waitFor(() => rendered.getByText('data: null')) await waitFor(() => rendered.getByText('data: hydrated')) - expect(states).toHaveLength(3) + expect(states).toHaveLength(2) expect(states[0]).toMatchObject({ status: 'loading', @@ -331,12 +331,6 @@ describe('PersistQueryClientProvider', () => { fetchStatus: 'idle', data: 'hydrated', }) - - expect(states[2]).toMatchObject({ - status: 'success', - fetchStatus: 'idle', - data: 'hydrated', - }) }) test('should call onSuccess after successful restoring', async () => { diff --git a/src/reactjs/tests/Hydrate.test.tsx b/src/reactjs/tests/Hydrate.test.tsx index 1bfd2d0c97..4baa7c9607 100644 --- a/src/reactjs/tests/Hydrate.test.tsx +++ b/src/reactjs/tests/Hydrate.test.tsx @@ -52,9 +52,8 @@ describe('React hydration', () => { ) - rendered.getByText('stringCached') - await sleep(10) - rendered.getByText('string') + await rendered.findByText('stringCached') + await rendered.findByText('string') queryClient.clear() }) @@ -89,9 +88,8 @@ describe('React hydration', () => { ) - rendered.getByText('stringCached') - await sleep(10) - rendered.getByText('string') + await rendered.findByText('stringCached') + await rendered.findByText('string') queryClientInner.clear() queryClientOuter.clear() @@ -121,8 +119,7 @@ describe('React hydration', () => { ) - await sleep(10) - rendered.getByText('string') + await rendered.findByText('string') const intermediateCache = new QueryCache() const intermediateClient = createQueryClient({ @@ -178,8 +175,7 @@ describe('React hydration', () => { ) - await sleep(10) - rendered.getByText('string') + await rendered.findByText('string') const newClientQueryCache = new QueryCache() const newClientQueryClient = createQueryClient({ diff --git a/src/reactjs/tests/QueryResetErrorBoundary.test.tsx b/src/reactjs/tests/QueryResetErrorBoundary.test.tsx index 90f40a8f10..d8b7568658 100644 --- a/src/reactjs/tests/QueryResetErrorBoundary.test.tsx +++ b/src/reactjs/tests/QueryResetErrorBoundary.test.tsx @@ -245,7 +245,7 @@ describe('QueryErrorResetBoundary', () => { await waitFor(() => rendered.getByText('status: loading, fetchStatus: idle') ) - rendered.getByRole('button', { name: /refetch/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('error boundary')) }) diff --git a/src/reactjs/tests/ssr-hydration.test.tsx b/src/reactjs/tests/ssr-hydration.test.tsx index 0dbfacf248..a60eebcbb9 100644 --- a/src/reactjs/tests/ssr-hydration.test.tsx +++ b/src/reactjs/tests/ssr-hydration.test.tsx @@ -1,6 +1,9 @@ import React from 'react' -import ReactDOM from 'react-dom' +import ReactDOM, { Root } from 'react-dom' +import ReactDOMTestUtils from 'react-dom/test-utils' import ReactDOMServer from 'react-dom/server' +// eslint-disable-next-line import/no-unresolved -- types only for module augmentation +import type {} from 'react-dom/next' import { useQuery, @@ -11,6 +14,25 @@ import { } from '../..' import { createQueryClient, mockLogger, setIsServer, sleep } from './utils' +const isReact18 = () => (process.env.REACTJS_VERSION || '18') === '18' + +const ReactHydrate = (element: React.ReactElement, container: Element) => { + if (isReact18()) { + let root: Root + ReactDOMTestUtils.act(() => { + root = ReactDOM.hydrateRoot(container, element) + }) + return () => { + root.unmount() + } + } + + ReactDOM.hydrate(element, container) + return () => { + ReactDOM.unmountComponentAtNode(container) + } +} + async function fetchData(value: TData, ms?: number): Promise { await sleep(ms || 1) return value @@ -21,7 +43,20 @@ function PrintStateComponent({ componentName, result }: any): any { } describe('Server side rendering with de/rehydration', () => { + let previousIsReactActEnvironment: unknown + beforeAll(() => { + // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist + previousIsReactActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT = true + }) + + afterAll(() => { + // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist + globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment + }) it('should not mismatch on success', async () => { + if (!isReact18()) { + return + } const fetchDataSuccess = jest.fn(fetchData) // -- Shared part -- @@ -61,6 +96,7 @@ describe('Server side rendering with de/rehydration', () => { 'SuccessComponent - status:success fetching:true data:success' expect(markup).toBe(expectedMarkup) + expect(fetchDataSuccess).toHaveBeenCalledTimes(1) // -- Client part -- const el = document.createElement('div') @@ -70,7 +106,7 @@ describe('Server side rendering with de/rehydration', () => { const queryClient = createQueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) - ReactDOM.hydrate( + const unmount = ReactHydrate( , @@ -79,14 +115,17 @@ describe('Server side rendering with de/rehydration', () => { // Check that we have no React hydration mismatches expect(mockLogger.error).not.toHaveBeenCalled() - expect(fetchDataSuccess).toHaveBeenCalledTimes(1) + expect(fetchDataSuccess).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) - ReactDOM.unmountComponentAtNode(el) + unmount() queryClient.clear() }) it('should not mismatch on error', async () => { + if (!isReact18()) { + return + } const fetchDataError = jest.fn(() => { throw new Error('fetchDataError') }) @@ -124,7 +163,7 @@ describe('Server side rendering with de/rehydration', () => { setIsServer(false) const expectedMarkup = - 'ErrorComponent - status:loading fetching:true data:undefined' + 'ErrorComponent - status:loading fetching:true data:undefined' expect(markup).toBe(expectedMarkup) @@ -136,7 +175,7 @@ describe('Server side rendering with de/rehydration', () => { const queryClient = createQueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) - ReactDOM.hydrate( + const unmount = ReactHydrate( , @@ -145,19 +184,22 @@ describe('Server side rendering with de/rehydration', () => { // We expect exactly one console.error here, which is from the expect(mockLogger.error).toHaveBeenCalledTimes(1) - expect(fetchDataError).toHaveBeenCalledTimes(1) + expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) await sleep(50) expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe( - 'ErrorComponent - status:error fetching:false data:undefined' + 'ErrorComponent - status:error fetching:false data:undefined' ) - ReactDOM.unmountComponentAtNode(el) + unmount() queryClient.clear() }) it('should not mismatch on queries that were not prefetched', async () => { + if (!isReact18()) { + return + } const fetchDataSuccess = jest.fn(fetchData) // -- Shared part -- @@ -171,15 +213,9 @@ describe('Server side rendering with de/rehydration', () => { // -- Server part -- setIsServer(true) - const prefetchCache = new QueryCache() - const prefetchClient = createQueryClient({ - queryCache: prefetchCache, - }) + const prefetchClient = createQueryClient() const dehydratedStateServer = dehydrate(prefetchClient) - const renderCache = new QueryCache() - const renderClient = createQueryClient({ - queryCache: renderCache, - }) + const renderClient = createQueryClient() hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( @@ -191,7 +227,7 @@ describe('Server side rendering with de/rehydration', () => { setIsServer(false) const expectedMarkup = - 'SuccessComponent - status:loading fetching:true data:undefined' + 'SuccessComponent - status:loading fetching:true data:undefined' expect(markup).toBe(expectedMarkup) @@ -203,7 +239,7 @@ describe('Server side rendering with de/rehydration', () => { const queryClient = createQueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) - ReactDOM.hydrate( + const unmount = ReactHydrate( , @@ -212,15 +248,15 @@ describe('Server side rendering with de/rehydration', () => { // Check that we have no React hydration mismatches expect(mockLogger.error).not.toHaveBeenCalled() - expect(fetchDataSuccess).toHaveBeenCalledTimes(0) + expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe(expectedMarkup) await sleep(50) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe( - 'SuccessComponent - status:success fetching:false data:success!' + 'SuccessComponent - status:success fetching:false data:success!' ) - ReactDOM.unmountComponentAtNode(el) + unmount() queryClient.clear() }) }) diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx index d6d4263317..729c817715 100644 --- a/src/reactjs/tests/suspense.test.tsx +++ b/src/reactjs/tests/suspense.test.tsx @@ -42,7 +42,10 @@ describe("useQuery's in Suspense mode", () => { states.push(state) return ( - + return ( +
+ + data: {state.data?.pages.join(',')} +
+ ) } const rendered = renderWithClient( @@ -91,7 +100,7 @@ describe("useQuery's in Suspense mode", () => { ) - await sleep(10) + await waitFor(() => rendered.getByText('data: 1')) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ @@ -100,7 +109,7 @@ describe("useQuery's in Suspense mode", () => { }) fireEvent.click(rendered.getByText('next')) - await sleep(10) + await waitFor(() => rendered.getByText('data: 2')) expect(states.length).toBe(2) expect(states[1]).toMatchObject({ @@ -210,8 +219,8 @@ describe("useQuery's in Suspense mode", () => { await waitFor(() => rendered.getByText('rendered')) - expect(successFn).toHaveBeenCalledTimes(1) - expect(successFn).toHaveBeenCalledWith('selected') + await waitFor(() => expect(successFn).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(successFn).toHaveBeenCalledWith('selected')) }) it('should call every onSuccess handler within a suspense boundary', async () => { @@ -255,8 +264,8 @@ describe("useQuery's in Suspense mode", () => { await waitFor(() => rendered.getByText('second')) - expect(successFn1).toHaveBeenCalledTimes(1) - expect(successFn2).toHaveBeenCalledTimes(1) + await waitFor(() => expect(successFn1).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(successFn2).toHaveBeenCalledTimes(1)) }) // https://github.com/tannerlinsley/react-query/issues/468 @@ -724,13 +733,21 @@ describe("useQuery's in Suspense mode", () => { const key = queryKey() const queryFn = jest.fn() - queryFn.mockImplementation(() => sleep(10)) + queryFn.mockImplementation(async () => { + await sleep(10) + return '23' + }) function Page() { const [enabled, setEnabled] = React.useState(false) - useQuery([key], queryFn, { suspense: true, enabled }) + const result = useQuery([key], queryFn, { suspense: true, enabled }) - return +

{result.data}

+ + ) } const rendered = renderWithClient( @@ -742,10 +759,13 @@ describe("useQuery's in Suspense mode", () => { expect(queryFn).toHaveBeenCalledTimes(0) - fireEvent.click(rendered.getByLabelText('fire')) + fireEvent.click(rendered.getByRole('button', { name: /fire/i })) + + await waitFor(() => { + expect(rendered.getByRole('heading').textContent).toBe('23') + }) expect(queryFn).toHaveBeenCalledTimes(1) - await waitFor(() => rendered.getByLabelText('fire')) }) it('should error catched in error boundary without infinite loop', async () => { diff --git a/src/reactjs/tests/useInfiniteQuery.test.tsx b/src/reactjs/tests/useInfiniteQuery.test.tsx index 411f6af727..25c70d02df 100644 --- a/src/reactjs/tests/useInfiniteQuery.test.tsx +++ b/src/reactjs/tests/useInfiniteQuery.test.tsx @@ -205,10 +205,10 @@ describe('useInfiniteQuery', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 0-desc')) - rendered.getByRole('button', { name: /fetchNextPage/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await waitFor(() => rendered.getByText('data: 0-desc,1-desc')) - rendered.getByRole('button', { name: /order/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /order/i })) await waitFor(() => rendered.getByText('data: 0-asc')) await waitFor(() => rendered.getByText('isFetching: false')) @@ -339,7 +339,10 @@ describe('useInfiniteQuery', () => { function Page() { const state = useInfiniteQuery( key, - ({ pageParam = 0 }) => Number(pageParam), + async ({ pageParam = 0 }) => { + await sleep(10) + return Number(pageParam) + }, { select: data => ({ pages: [...data.pages].reverse(), @@ -351,22 +354,25 @@ describe('useInfiniteQuery', () => { states.push(state) - const { fetchNextPage } = state - - React.useEffect(() => { - setActTimeout(() => { - fetchNextPage({ pageParam: 1 }) - }, 10) - }, [fetchNextPage]) - - return null + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {state.isFetching}
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(100) + await waitFor(() => rendered.getByText('data: 0')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) - expect(states.length).toBe(4) + await waitFor(() => rendered.getByText('data: 1,0')) + + await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, @@ -464,36 +470,43 @@ describe('useInfiniteQuery', () => { const states: UseInfiniteQueryResult[] = [] function Page() { - const state = useInfiniteQuery( - key, - ({ pageParam = 10 }) => Number(pageParam), - { notifyOnChangeProps: 'all' } - ) + const state = useInfiniteQuery(key, async ({ pageParam = 10 }) => { + await sleep(10) + return Number(pageParam) + }) states.push(state) - const { fetchNextPage, fetchPreviousPage, refetch } = state + return ( +
+ + + +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } - React.useEffect(() => { - setActTimeout(() => { - fetchNextPage({ pageParam: 11 }) - }, 10) - setActTimeout(() => { - fetchPreviousPage({ pageParam: 9 }) - }, 20) - setActTimeout(() => { - refetch() - }, 30) - }, [fetchNextPage, fetchPreviousPage, refetch]) + const rendered = renderWithClient(queryClient, ) - return null - } + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) - renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('data: 10,11')) + fireEvent.click( + rendered.getByRole('button', { name: /fetchPreviousPage/i }) + ) + await waitFor(() => rendered.getByText('data: 9,10,11')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) - await sleep(100) + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(8)) - expect(states.length).toBe(8) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, @@ -555,7 +568,10 @@ describe('useInfiniteQuery', () => { function Page() { const state = useInfiniteQuery( key, - ({ pageParam = 10 }) => Number(pageParam), + async ({ pageParam = 10 }) => { + await sleep(10) + return Number(pageParam) + }, { getPreviousPageParam: firstPage => firstPage - 1, getNextPageParam: lastPage => lastPage + 1, @@ -565,28 +581,34 @@ describe('useInfiniteQuery', () => { states.push(state) - const { fetchNextPage, fetchPreviousPage, refetch } = state + return ( +
+ + + +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } - React.useEffect(() => { - setActTimeout(() => { - fetchNextPage() - }, 10) - setActTimeout(() => { - fetchPreviousPage() - }, 20) - setActTimeout(() => { - refetch() - }, 30) - }, [fetchNextPage, fetchPreviousPage, refetch]) + const rendered = renderWithClient(queryClient, ) - return null - } + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) - renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('data: 10,11')) + fireEvent.click( + rendered.getByRole('button', { name: /fetchPreviousPage/i }) + ) + await waitFor(() => rendered.getByText('data: 9,10,11')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) - await sleep(100) + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(8)) - expect(states.length).toBe(8) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, @@ -649,7 +671,10 @@ describe('useInfiniteQuery', () => { const multiplier = React.useRef(1) const state = useInfiniteQuery( key, - ({ pageParam = 10 }) => Number(pageParam) * multiplier.current, + async ({ pageParam = 10 }) => { + await sleep(10) + return Number(pageParam) * multiplier.current + }, { getNextPageParam: lastPage => lastPage + 1, notifyOnChangeProps: 'all', @@ -658,28 +683,37 @@ describe('useInfiniteQuery', () => { states.push(state) - const { fetchNextPage, refetch } = state + return ( +
+ + +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } - React.useEffect(() => { - setActTimeout(() => { - fetchNextPage() - }, 10) - setActTimeout(() => { - multiplier.current = 2 - refetch({ - refetchPage: (_, index) => index === 0, - }) - }, 20) - }, [fetchNextPage, refetch]) + const rendered = renderWithClient(queryClient, ) - return null - } + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) - renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('data: 10,11')) + fireEvent.click(rendered.getByRole('button', { name: /refetchPage/i })) - await sleep(50) + await waitFor(() => rendered.getByText('data: 20,11')) + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(6)) - expect(states.length).toBe(6) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, @@ -1154,7 +1188,7 @@ describe('useInfiniteQuery', () => { await sleep(100) - expect(states.length).toBe(6) + expect(states.length).toBe(5) expect(states[0]).toMatchObject({ hasNextPage: undefined, data: undefined, @@ -1178,16 +1212,8 @@ describe('useInfiniteQuery', () => { isFetchingNextPage: false, isSuccess: true, }) - // Hook state update - expect(states[3]).toMatchObject({ - hasNextPage: true, - data: { pages: [7, 8] }, - isFetching: false, - isFetchingNextPage: false, - isSuccess: true, - }) // Refetch - expect(states[4]).toMatchObject({ + expect(states[3]).toMatchObject({ hasNextPage: true, data: { pages: [7, 8] }, isFetching: true, @@ -1195,7 +1221,7 @@ describe('useInfiniteQuery', () => { isSuccess: true, }) // Refetch done - expect(states[5]).toMatchObject({ + expect(states[4]).toMatchObject({ hasNextPage: true, data: { pages: [7, 8] }, isFetching: false, diff --git a/src/reactjs/tests/useIsFetching.test.tsx b/src/reactjs/tests/useIsFetching.test.tsx index ac243554c8..78fdfcfae5 100644 --- a/src/reactjs/tests/useIsFetching.test.tsx +++ b/src/reactjs/tests/useIsFetching.test.tsx @@ -1,10 +1,10 @@ -import { fireEvent, waitFor } from '@testing-library/react' +import { fireEvent, waitFor, screen } from '@testing-library/react' +import '@testing-library/jest-dom' import React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { createQueryClient, - mockLogger, queryKey, renderWithClient, setActTimeout, @@ -27,7 +27,7 @@ describe('useIsFetching', () => { useQuery( key, async () => { - await sleep(1000) + await sleep(50) return 'test' }, { @@ -43,12 +43,12 @@ describe('useIsFetching', () => { ) } - const rendered = renderWithClient(queryClient, ) + const { findByText, getByRole } = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('isFetching: 0')) - fireEvent.click(rendered.getByText('setReady')) - await waitFor(() => rendered.getByText('isFetching: 1')) - await waitFor(() => rendered.getByText('isFetching: 0')) + await findByText('isFetching: 0') + fireEvent.click(getByRole('button', { name: /setReady/i })) + await findByText('isFetching: 1') + await findByText('isFetching: 0') }) it('should not update state while rendering', async () => { @@ -93,26 +93,22 @@ describe('useIsFetching', () => { return ( <> + {renderSecond && } - ) } renderWithClient(queryClient, ) await waitFor(() => expect(isFetchings).toEqual([0, 1, 1, 2, 1, 0])) - expect(mockLogger.error).not.toHaveBeenCalled() }) it('should be able to filter', async () => { - const queryCache = new QueryCache() - const queryClient = createQueryClient({ queryCache }) + const queryClient = createQueryClient() const key1 = queryKey() const key2 = queryKey() - const isFetchings: number[] = [] - function One() { useQuery(key1, async () => { await sleep(10) @@ -132,30 +128,30 @@ describe('useIsFetching', () => { function Page() { const [started, setStarted] = React.useState(false) const isFetching = useIsFetching(key1) - isFetchings.push(isFetching) - - React.useEffect(() => { - setActTimeout(() => { - setStarted(true) - }, 5) - }, []) - - if (!started) { - return null - } return (
- - + +
isFetching: {isFetching}
+ {started ? ( + <> + + + + ) : null}
) } - renderWithClient(queryClient, ) + const { findByText, getByRole } = renderWithClient(queryClient, ) - await sleep(100) - expect(isFetchings).toEqual([0, 0, 1, 0]) + await findByText('isFetching: 0') + fireEvent.click(getByRole('button', { name: /setStarted/i })) + await findByText('isFetching: 1') + await waitFor(() => { + expect(screen.queryByText('isFetching: 2')).not.toBeInTheDocument() + }) + await findByText('isFetching: 0') }) describe('with custom context', () => { @@ -174,7 +170,7 @@ describe('useIsFetching', () => { useQuery( key, async () => { - await sleep(1000) + await sleep(50) return 'test' }, { @@ -191,14 +187,18 @@ describe('useIsFetching', () => { ) } - const rendered = renderWithClient(queryClient, , { - context, - }) + const { findByText, getByRole } = renderWithClient( + queryClient, + , + { + context, + } + ) - await waitFor(() => rendered.getByText('isFetching: 0')) - fireEvent.click(rendered.getByText('setReady')) - await waitFor(() => rendered.getByText('isFetching: 1')) - await waitFor(() => rendered.getByText('isFetching: 0')) + await findByText('isFetching: 0') + fireEvent.click(getByRole('button', { name: /setReady/i })) + await findByText('isFetching: 1') + await findByText('isFetching: 0') }) it('should throw if the context is not passed to useIsFetching', async () => { diff --git a/src/reactjs/tests/useIsMutating.test.tsx b/src/reactjs/tests/useIsMutating.test.tsx index cf08f96c00..87a3b5b6ee 100644 --- a/src/reactjs/tests/useIsMutating.test.tsx +++ b/src/reactjs/tests/useIsMutating.test.tsx @@ -44,7 +44,7 @@ describe('useIsMutating', () => { } renderWithClient(queryClient, ) - await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 2, 2, 1, 0])) + await waitFor(() => expect(isMutatings).toEqual([0, 1, 2, 1, 0])) }) it('should filter correctly by mutationKey', async () => { @@ -76,7 +76,7 @@ describe('useIsMutating', () => { } renderWithClient(queryClient, ) - await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 1, 0, 0])) + await waitFor(() => expect(isMutatings).toEqual([0, 1, 0])) }) it('should filter correctly by predicate', async () => { @@ -111,7 +111,7 @@ describe('useIsMutating', () => { } renderWithClient(queryClient, ) - await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 1, 0, 0])) + await waitFor(() => expect(isMutatings).toEqual([0, 1, 0])) }) it('should not change state if unmounted', async () => { @@ -208,7 +208,7 @@ describe('useIsMutating', () => { } renderWithClient(queryClient, , { context }) - await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 2, 2, 1, 0])) + await waitFor(() => expect(isMutatings).toEqual([0, 1, 2, 1, 0])) }) it('should throw if the context is not passed to useIsMutating', async () => { diff --git a/src/reactjs/tests/useMutation.test.tsx b/src/reactjs/tests/useMutation.test.tsx index 5f973b5e88..29ea5c463a 100644 --- a/src/reactjs/tests/useMutation.test.tsx +++ b/src/reactjs/tests/useMutation.test.tsx @@ -21,34 +21,34 @@ describe('useMutation', () => { it('should be able to reset `data`', async () => { function Page() { - const { mutate, data = '', reset } = useMutation(() => + const { mutate, data = 'empty', reset } = useMutation(() => Promise.resolve('mutation') ) return (
-

{data}

+

{data}

) } - const { getByTestId, getByText } = renderWithClient(queryClient, ) + const { getByRole } = renderWithClient(queryClient, ) - expect(getByTestId('title').textContent).toBe('') + expect(getByRole('heading').textContent).toBe('empty') - fireEvent.click(getByText('mutate')) - - await waitFor(() => getByTestId('title')) + fireEvent.click(getByRole('button', { name: /mutate/i })) - expect(getByTestId('title').textContent).toBe('mutation') - - fireEvent.click(getByText('reset')) + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('mutation') + }) - await waitFor(() => getByTestId('title')) + fireEvent.click(getByRole('button', { name: /reset/i })) - expect(getByTestId('title').textContent).toBe('') + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('empty') + }) }) it('should be able to reset `error`', async () => { @@ -61,31 +61,32 @@ describe('useMutation', () => { return (
- {error &&

{error.message}

} + {error &&

{error.message}

}
) } - const { getByTestId, getByText, queryByTestId } = renderWithClient( - queryClient, - - ) + const { getByRole, queryByRole } = renderWithClient(queryClient, ) - expect(queryByTestId('error')).toBeNull() - - fireEvent.click(getByText('mutate')) + await waitFor(() => { + expect(queryByRole('heading')).toBeNull() + }) - await waitFor(() => getByTestId('error')) + fireEvent.click(getByRole('button', { name: /mutate/i })) - expect(getByTestId('error').textContent).toBe( - 'Expected mock error. All is well!' - ) + await waitFor(() => { + expect(getByRole('heading').textContent).toBe( + 'Expected mock error. All is well!' + ) + }) - fireEvent.click(getByText('reset')) + fireEvent.click(getByRole('button', { name: /reset/i })) - await waitFor(() => expect(queryByTestId('error')).toBeNull()) + await waitFor(() => { + expect(queryByRole('heading')).toBeNull() + }) }) it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => { @@ -95,7 +96,7 @@ describe('useMutation', () => { function Page() { const { mutate } = useMutation( - async (vars: { count: number }) => Promise.resolve(vars.count), + (vars: { count: number }) => Promise.resolve(vars.count), { onSuccess: data => { onSuccessMock(data) @@ -108,33 +109,39 @@ describe('useMutation', () => { return (
-

{count}

+

{count}

) } - const { getByTestId, getByText } = renderWithClient(queryClient, ) + const { getByRole } = renderWithClient(queryClient, ) - expect(getByTestId('title').textContent).toBe('0') + expect(getByRole('heading').textContent).toBe('0') - fireEvent.click(getByText('mutate')) - fireEvent.click(getByText('mutate')) - fireEvent.click(getByText('mutate')) + fireEvent.click(getByRole('button', { name: /mutate/i })) + fireEvent.click(getByRole('button', { name: /mutate/i })) + fireEvent.click(getByRole('button', { name: /mutate/i })) - await waitFor(() => getByTestId('title')) + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('3') + }) + + await waitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(3) + }) - expect(onSuccessMock).toHaveBeenCalledTimes(3) expect(onSuccessMock).toHaveBeenCalledWith(1) expect(onSuccessMock).toHaveBeenCalledWith(2) expect(onSuccessMock).toHaveBeenCalledWith(3) - expect(onSettledMock).toHaveBeenCalledTimes(3) + await waitFor(() => { + expect(onSettledMock).toHaveBeenCalledTimes(3) + }) + expect(onSettledMock).toHaveBeenCalledWith(1) expect(onSettledMock).toHaveBeenCalledWith(2) expect(onSettledMock).toHaveBeenCalledWith(3) - - expect(getByTestId('title').textContent).toBe('3') }) it('should be able to call `onError` and `onSettled` after each failed mutate', async () => { @@ -163,23 +170,27 @@ describe('useMutation', () => { return (
-

{count}

+

{count}

) } - const { getByTestId, getByText } = renderWithClient(queryClient, ) + const { getByRole } = renderWithClient(queryClient, ) - expect(getByTestId('title').textContent).toBe('0') + expect(getByRole('heading').textContent).toBe('0') - fireEvent.click(getByText('mutate')) - fireEvent.click(getByText('mutate')) - fireEvent.click(getByText('mutate')) + fireEvent.click(getByRole('button', { name: /mutate/i })) + fireEvent.click(getByRole('button', { name: /mutate/i })) + fireEvent.click(getByRole('button', { name: /mutate/i })) - await waitFor(() => getByTestId('title')) + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('3') + }) - expect(onErrorMock).toHaveBeenCalledTimes(3) + await waitFor(() => { + expect(onErrorMock).toHaveBeenCalledTimes(3) + }) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1' ) @@ -190,7 +201,9 @@ describe('useMutation', () => { 'Expected mock error. All is well! 3' ) - expect(onSettledMock).toHaveBeenCalledTimes(3) + await waitFor(() => { + expect(onSettledMock).toHaveBeenCalledTimes(3) + }) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1' ) @@ -200,8 +213,6 @@ describe('useMutation', () => { expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 3' ) - - expect(getByTestId('title').textContent).toBe('3') }) it('should be able to override the useMutation success callbacks', async () => { @@ -302,7 +313,10 @@ describe('useMutation', () => { const key = queryKey() queryClient.setMutationDefaults(key, { - mutationFn: async (text: string) => text, + mutationFn: async (text: string) => { + await sleep(10) + return text + }, }) const states: UseMutationResult[] = [] @@ -401,7 +415,7 @@ describe('useMutation', () => { ).toBeInTheDocument() }) - rendered.getByRole('button', { name: /mutate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await waitFor(() => { expect( @@ -459,7 +473,7 @@ describe('useMutation', () => { await rendered.findByText('data: null, status: idle, isPaused: false') - rendered.getByRole('button', { name: /mutate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await rendered.findByText('data: null, status: loading, isPaused: true') @@ -506,7 +520,7 @@ describe('useMutation', () => { await rendered.findByText('data: null, status: idle, isPaused: false') - rendered.getByRole('button', { name: /mutate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await rendered.findByText('data: null, status: loading, isPaused: true') @@ -530,7 +544,8 @@ describe('useMutation', () => { function Page() { const state = useMutation( - (_text: string) => { + async (_text: string) => { + await sleep(1) count++ return count > 1 ? Promise.resolve('data') : Promise.reject('oops') }, @@ -827,8 +842,8 @@ describe('useMutation', () => { await rendered.findByText('data: null, status: idle, isPaused: false') - rendered.getByRole('button', { name: /mutate/i }).click() - rendered.getByRole('button', { name: /hide/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) + fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await waitFor(() => { expect( @@ -849,39 +864,35 @@ describe('useMutation', () => { const context = React.createContext(undefined) function Page() { - const { mutate, data = '', reset } = useMutation( + const { mutate, data = 'empty', reset } = useMutation( () => Promise.resolve('mutation'), { context } ) return (
-

{data}

+

{data}

) } - const { getByTestId, getByText } = renderWithClient( - queryClient, - , - { context } - ) - - expect(getByTestId('title').textContent).toBe('') + const { getByRole } = renderWithClient(queryClient, , { context }) - fireEvent.click(getByText('mutate')) + expect(getByRole('heading').textContent).toBe('empty') - await waitFor(() => getByTestId('title')) + fireEvent.click(getByRole('button', { name: /mutate/i })) - expect(getByTestId('title').textContent).toBe('mutation') - - fireEvent.click(getByText('reset')) + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('mutation') + }) - await waitFor(() => getByTestId('title')) + fireEvent.click(getByRole('button', { name: /reset/i })) - expect(getByTestId('title').textContent).toBe('') + await waitFor(() => { + expect(getByRole('heading').textContent).toBe('empty') + }) }) it('should throw if the context is not passed to useMutation', async () => { @@ -952,8 +963,8 @@ describe('useMutation', () => { await rendered.findByText('data: null, status: idle') - rendered.getByRole('button', { name: /mutate/i }).click() - rendered.getByRole('button', { name: /mutate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) + fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await rendered.findByText('data: result2, status: success') diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx index c132a75d2f..cb18fb8f9b 100644 --- a/src/reactjs/tests/useQueries.test.tsx +++ b/src/reactjs/tests/useQueries.test.tsx @@ -1,4 +1,4 @@ -import { waitFor, fireEvent } from '@testing-library/react' +import { fireEvent, waitFor } from '@testing-library/react' import React from 'react' import { ErrorBoundary } from 'react-error-boundary' @@ -10,7 +10,6 @@ import { expectTypeNotAny, queryKey, renderWithClient, - setActTimeout, sleep, } from './utils' import { @@ -41,26 +40,33 @@ describe('useQueries', () => { { queryKey: key1, queryFn: async () => { - await sleep(5) + await sleep(10) return 1 }, }, { queryKey: key2, queryFn: async () => { - await sleep(10) + await sleep(100) return 2 }, }, ], }) results.push(result) - return null + + return ( +
+
+ data1: {result[0].data ?? 'null'}, data2: {result[1].data ?? 'null'} +
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(30) + await waitFor(() => rendered.getByText('data1: 1, data2: 2')) expect(results.length).toBe(3) expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }]) @@ -81,7 +87,7 @@ describe('useQueries', () => { queryKey: [key1, count], keepPreviousData: true, queryFn: async () => { - await sleep(5) + await sleep(10) return count * 2 }, }, @@ -89,7 +95,7 @@ describe('useQueries', () => { queryKey: [key2, count], keepPreviousData: true, queryFn: async () => { - await sleep(10) + await sleep(35) return count * 5 }, }, @@ -97,59 +103,28 @@ describe('useQueries', () => { }) states.push(result) - React.useEffect(() => { - setActTimeout(() => { - setCount(prev => prev + 1) - }, 20) - }, []) + const isFetching = result.some(r => r.isFetching) - return null + return ( +
+
+ data1: {result[0].data ?? 'null'}, data2: {result[1].data ?? 'null'} +
+
isFetching: {String(isFetching)}
+ +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await waitFor(() => expect(states.length).toBe(7)) + await waitFor(() => rendered.getByText('data1: 2, data2: 5')) + fireEvent.click(rendered.getByRole('button', { name: /inc/i })) - expect(states[0]).toMatchObject([ - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { status: 'success', data: 2, isPreviousData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[2]).toMatchObject([ - { status: 'success', data: 2, isPreviousData: false, isFetching: false }, - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - ]) - expect(states[3]).toMatchObject([ - { status: 'success', data: 2, isPreviousData: true, isFetching: true }, - { status: 'success', data: 5, isPreviousData: true, isFetching: true }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 2, isPreviousData: true, isFetching: true }, - { status: 'success', data: 5, isPreviousData: true, isFetching: true }, - ]) - expect(states[5]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: false, isFetching: false }, - { status: 'success', data: 5, isPreviousData: true, isFetching: true }, - ]) - expect(states[6]).toMatchObject([ + await waitFor(() => rendered.getByText('data1: 4, data2: 10')) + await waitFor(() => rendered.getByText('isFetching: false')) + + expect(states[states.length - 1]).toMatchObject([ { status: 'success', data: 4, isPreviousData: false, isFetching: false }, { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) @@ -166,7 +141,7 @@ describe('useQueries', () => { queryKey: [key, count, i + 1], keepPreviousData: true, queryFn: async () => { - await sleep(5 * (i + 1)) + await sleep(35 * (i + 1)) return (i + 1) * count * 2 }, })), @@ -174,88 +149,26 @@ describe('useQueries', () => { states.push(result) - React.useEffect(() => { - setActTimeout(() => { - setCount(prev => prev + 1) - }, 20) - }, []) + const isFetching = result.some(r => r.isFetching) - return null + return ( +
+
data: {result.map(it => it.data).join(',')}
+
isFetching: {String(isFetching)}
+ +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await waitFor(() => expect(states.length).toBe(8)) + await waitFor(() => rendered.getByText('data: 4,8')) + fireEvent.click(rendered.getByRole('button', { name: /inc/i })) - expect(states[0]).toMatchObject([ - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[2]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: false, isFetching: false }, - { status: 'success', data: 8, isPreviousData: false, isFetching: false }, - ]) + await waitFor(() => rendered.getByText('data: 6,12,18')) + await waitFor(() => rendered.getByText('isFetching: false')) - expect(states[3]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: true, isFetching: true }, - { status: 'success', data: 8, isPreviousData: true, isFetching: true }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: true, isFetching: true }, - { status: 'success', data: 8, isPreviousData: true, isFetching: true }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[5]).toMatchObject([ - { status: 'success', data: 6, isPreviousData: false, isFetching: false }, - { status: 'success', data: 8, isPreviousData: true, isFetching: true }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[6]).toMatchObject([ - { status: 'success', data: 6, isPreviousData: false, isFetching: false }, - { status: 'success', data: 12, isPreviousData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[7]).toMatchObject([ + expect(states[states.length - 1]).toMatchObject([ { status: 'success', data: 6, isPreviousData: false, isFetching: false }, { status: 'success', data: 12, isPreviousData: false, isFetching: false }, { status: 'success', data: 18, isPreviousData: false, isFetching: false }, @@ -304,10 +217,10 @@ describe('useQueries', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data1: 5, data2: 10')) - rendered.getByRole('button', { name: /setSeries2/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /setSeries2/i })) await waitFor(() => rendered.getByText('data1: 5, data2: 15')) - rendered.getByRole('button', { name: /setSeries1/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /setSeries1/i })) await waitFor(() => rendered.getByText('data1: 10, data2: 15')) await waitFor(() => rendered.getByText('isFetching: false')) @@ -341,22 +254,34 @@ describe('useQueries', () => { states.push(result) - React.useEffect(() => { - setActTimeout(() => { - setEnableId1(false) - }, 20) - - setActTimeout(() => { - setEnableId1(true) - }, 30) - }, []) + const isFetching = result.some(r => r.isFetching) - return null + return ( +
+
+ data1: {result[0]?.data ?? 'null'}, data2:{' '} + {result[1]?.data ?? 'null'} +
+
isFetching: {String(isFetching)}
+ + +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data1: 5, data2: 10')) + fireEvent.click(rendered.getByRole('button', { name: /set1Disabled/i })) + + await waitFor(() => rendered.getByText('data1: 10, data2: null')) + await waitFor(() => rendered.getByText('isFetching: false')) + fireEvent.click(rendered.getByRole('button', { name: /set2Enabled/i })) - await waitFor(() => expect(states.length).toBe(8)) + await waitFor(() => rendered.getByText('data1: 5, data2: 10')) + await waitFor(() => rendered.getByText('isFetching: false')) + + await waitFor(() => expect(states.length).toBe(6)) expect(states[0]).toMatchObject([ { @@ -374,32 +299,20 @@ describe('useQueries', () => { ]) expect(states[1]).toMatchObject([ { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, + { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) expect(states[2]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) expect(states[3]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[5]).toMatchObject([ { status: 'success', data: 5, isPreviousData: false, isFetching: true }, { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) - expect(states[6]).toMatchObject([ + expect(states[4]).toMatchObject([ { status: 'success', data: 5, isPreviousData: false, isFetching: true }, { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) - expect(states[7]).toMatchObject([ + expect(states[5]).toMatchObject([ { status: 'success', data: 5, isPreviousData: false, isFetching: false }, { status: 'success', data: 10, isPreviousData: false, isFetching: false }, ]) diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index 6951a81e0c..39adb0d723 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -137,7 +137,10 @@ describe('useQuery', () => { const states: UseQueryResult[] = [] function Page() { - const state = useQuery(key, () => 'test') + const state = useQuery(key, async () => { + await sleep(10) + return 'test' + }) states.push(state) @@ -158,9 +161,11 @@ describe('useQuery', () => { return {state.data} } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(10) + await waitFor(() => rendered.getByText('test')) + + expect(states.length).toEqual(2) expect(states[0]).toEqual({ data: undefined, @@ -372,10 +377,14 @@ describe('useQuery', () => { const onSuccess = jest.fn() function Page() { - const state = useQuery(key, () => 'data', { - onSuccess, - notifyOnChangeProps: 'all', - }) + const state = useQuery( + key, + async () => { + await sleep(10) + return 'data' + }, + { onSuccess, notifyOnChangeProps: 'all' } + ) states.push(state) @@ -384,7 +393,7 @@ describe('useQuery', () => { React.useEffect(() => { setActTimeout(() => { refetch() - }, 10) + }, 20) }, [refetch]) return null @@ -487,7 +496,7 @@ describe('useQuery', () => { const onError = jest.fn() function Page() { - useQuery( + const { status, fetchStatus } = useQuery( key, async () => { await sleep(10) @@ -497,13 +506,21 @@ describe('useQuery', () => { onError, } ) - return null + return ( + + status: {status}, fetchStatus: {fetchStatus} + + ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) await sleep(5) await queryClient.cancelQueries(key) + // query cancellation will reset the query to it's initial state + await waitFor(() => + rendered.getByText('status: loading, fetchStatus: idle') + ) expect(onError).not.toHaveBeenCalled() }) @@ -714,7 +731,7 @@ describe('useQuery', () => { await rendered.findByText('data: 1') - rendered.getByRole('button', { name: /toggle/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /toggle/i })) await rendered.findByText('data: 2') @@ -945,16 +962,21 @@ describe('useQuery', () => { const states: UseQueryResult[] = [] function Page() { - const state = useQuery(key, () => 'test') + const state = useQuery(key, async () => { + await sleep(10) + return 'test' + }) states.push(state) const { refetch, data } = state React.useEffect(() => { - if (data) { - refetch() - } + setActTimeout(() => { + if (data) { + refetch() + } + }, 20) }, [refetch, data]) return ( @@ -1016,62 +1038,71 @@ describe('useQuery', () => { const { remove } = state - React.useEffect(() => { - setActTimeout(() => { - remove() - }, 5) - setActTimeout(() => { - rerender({}) - }, 10) - }, [remove, rerender]) - - return null + return ( +
+ + + data: {state.data ?? 'null'} +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /remove/i })) await sleep(20) + fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) + await waitFor(() => rendered.getByText('data: 2')) - expect(states.length).toBe(5) + expect(states.length).toBe(4) // Initial - expect(states[0]).toMatchObject({ data: undefined }) + expect(states[0]).toMatchObject({ status: 'loading', data: undefined }) // Fetched - expect(states[1]).toMatchObject({ data: 1 }) - // Remove - expect(states[2]).toMatchObject({ data: undefined }) - // Hook state update - expect(states[3]).toMatchObject({ data: undefined }) + expect(states[1]).toMatchObject({ status: 'success', data: 1 }) + // Remove + Hook state update, batched + expect(states[2]).toMatchObject({ status: 'loading', data: undefined }) // Fetched - expect(states[4]).toMatchObject({ data: 2 }) + expect(states[3]).toMatchObject({ status: 'success', data: 2 }) }) - it('should be create a new query when refetching a removed query', async () => { + it('should create a new query when refetching a removed query', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { - const state = useQuery(key, () => ++count, { notifyOnChangeProps: 'all' }) + const state = useQuery( + key, + async () => { + await sleep(10) + return ++count + }, + { notifyOnChangeProps: 'all' } + ) states.push(state) const { remove, refetch } = state - React.useEffect(() => { - setActTimeout(() => { - remove() - }, 5) - setActTimeout(() => { - refetch() - }, 10) - }, [remove, refetch]) - - return null + return ( +
+ + + data: {state.data ?? 'null'} +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(20) + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /remove/i })) + + await sleep(50) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + await waitFor(() => rendered.getByText('data: 2')) expect(states.length).toBe(4) // Initial @@ -1104,7 +1135,8 @@ describe('useQuery', () => { function Page() { const state = useQuery( key, - () => { + async () => { + await sleep(10) count++ return count === 1 ? result1 : result2 }, @@ -1115,15 +1147,20 @@ describe('useQuery', () => { const { refetch } = state - React.useEffect(() => { - setActTimeout(() => { - refetch() - }, 10) - }, [refetch]) - return null + return ( +
+ + data: {String(state.data?.[1]?.done)} +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: false')) + await sleep(20) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + await waitFor(() => rendered.getByText('data: true')) await waitFor(() => expect(states.length).toBe(4)) @@ -1154,7 +1191,7 @@ describe('useQuery', () => { const result = useQuery( key, async () => { - await sleep(1) + await sleep(10) return 'fetched' }, { @@ -1165,20 +1202,25 @@ describe('useQuery', () => { results.push(result) - React.useEffect(() => { - setActTimeout(() => { - queryClient.refetchQueries(key) - }, 10) - }, []) - - return null + return ( +
+
isFetching: {result.isFetching}
+ + data: {result.data} +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(50) + await waitFor(() => rendered.getByText('data: set')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + await waitFor(() => rendered.getByText('data: fetched')) + + await waitFor(() => expect(results.length).toBe(3)) - expect(results.length).toBe(3) expect(results[0]).toMatchObject({ data: 'set', isFetching: false }) expect(results[1]).toMatchObject({ data: 'set', isFetching: true }) expect(results[2]).toMatchObject({ data: 'fetched', isFetching: false }) @@ -1193,29 +1235,33 @@ describe('useQuery', () => { const state = useQuery( key, async () => { - await sleep(1) + await sleep(10) count++ return count }, - { staleTime: Infinity } + { staleTime: Infinity, notifyOnChangeProps: 'all' } ) states.push(state) - React.useEffect(() => { - setActTimeout(() => { - queryClient.invalidateQueries(key) - }, 10) - }, []) - - return null + return ( +
+ + data: {state.data} +
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(100) + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) + await waitFor(() => rendered.getByText('data: 2')) + + await waitFor(() => expect(states.length).toBe(4)) - expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, @@ -1455,6 +1501,7 @@ describe('useQuery', () => { const state = useQuery( [key, count], async () => { + await sleep(10) if (count === 2) { throw new Error('Error test') } @@ -1484,7 +1531,7 @@ describe('useQuery', () => { act(() => rendered.rerender()) await waitFor(() => rendered.getByText('error: Error test')) - expect(states.length).toBe(8) + await waitFor(() => expect(states.length).toBe(8)) // Initial expect(states[0]).toMatchObject({ data: undefined, @@ -1791,18 +1838,24 @@ describe('useQuery', () => { const states: UseQueryResult[] = [] function FirstComponent() { - const state = useQuery(key, () => 1, { notifyOnChangeProps: 'all' }) + const state = useQuery( + key, + async () => { + await sleep(10) + return 1 + }, + { notifyOnChangeProps: 'all' } + ) const refetch = state.refetch states.push(state) - React.useEffect(() => { - setActTimeout(() => { - refetch() - }, 10) - }, [refetch]) - - return null + return ( +
+ + data: {state.data} +
+ ) } function SecondComponent() { @@ -1819,7 +1872,10 @@ describe('useQuery', () => { ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => expect(states.length).toBe(4)) @@ -1843,22 +1899,39 @@ describe('useQuery', () => { const states1: UseQueryResult[] = [] const states2: UseQueryResult[] = [] - await queryClient.prefetchQuery(key, () => 'prefetch') + await queryClient.prefetchQuery(key, async () => { + await sleep(10) + return 'prefetch' + }) await sleep(20) function FirstComponent() { - const state = useQuery(key, () => 'one', { - staleTime: 100, - }) + const state = useQuery( + key, + async () => { + await sleep(10) + return 'one' + }, + { + staleTime: 100, + } + ) states1.push(state) return null } function SecondComponent() { - const state = useQuery(key, () => 'two', { - staleTime: 10, - }) + const state = useQuery( + key, + async () => { + await sleep(10) + return 'two' + }, + { + staleTime: 10, + } + ) states2.push(state) return null } @@ -2238,7 +2311,7 @@ describe('useQuery', () => { let renders = 0 const queryFn = async () => { - await sleep(10) + await sleep(15) return 'data' } @@ -2261,7 +2334,7 @@ describe('useQuery', () => { const key = queryKey() let renders = 0 - let renderedCount = 0 + let callbackCount = 0 const queryFn = async () => { await sleep(10) @@ -2280,20 +2353,24 @@ describe('useQuery', () => { setCount(x => x + 1) }, }) - renders++ - renderedCount = count - return null + + React.useEffect(() => { + renders++ + callbackCount = count + }) + + return
count: {count}
} - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(20) + await waitFor(() => rendered.getByText('count: 2')) - // Should be 2 instead of 5 - expect(renders).toBe(2) + // Should be 2 / 3 instead of 5, uSES batches differently + expect(renders).toBe(process.env.REACTJS_VERSION === '17' ? 2 : 3) // Both callbacks should have been executed - expect(renderedCount).toBe(2) + expect(callbackCount).toBe(2) }) it('should render latest data even if react has discarded certain renders', async () => { @@ -2486,7 +2563,7 @@ describe('useQuery', () => { const state = useQuery( key, async () => { - await sleep(1) + await sleep(10) return count++ }, { @@ -2500,15 +2577,15 @@ describe('useQuery', () => { renderWithClient(queryClient, ) - await sleep(10) + await sleep(20) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) - await sleep(10) + await sleep(20) - expect(states.length).toBe(4) + await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) expect(states[2]).toMatchObject({ data: 0, isFetching: true }) @@ -2770,8 +2847,9 @@ describe('useQuery', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('failureCount: 1')) - rendered.getByRole('button', { name: /hide/i }).click() - rendered.getByRole('button', { name: /show/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /hide/i })) + await waitFor(() => rendered.getByRole('button', { name: /show/i })) + fireEvent.click(rendered.getByRole('button', { name: /show/i })) await waitFor(() => rendered.getByText('error: some error')) expect(count).toBe(3) @@ -2820,9 +2898,10 @@ describe('useQuery', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('failureCount: 1')) - rendered.getByRole('button', { name: /hide/i }).click() - rendered.getByRole('button', { name: /cancel/i }).click() - rendered.getByRole('button', { name: /show/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /hide/i })) + fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) + await waitFor(() => rendered.getByRole('button', { name: /show/i })) + fireEvent.click(rendered.getByRole('button', { name: /show/i })) await waitFor(() => rendered.getByText('error: some error')) // initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4 @@ -2841,13 +2920,20 @@ describe('useQuery', () => { staleTime: 50, }) states.push(state) - return null + return ( +
+
data: {state.data ?? 'null'}
+
isFetching: {state.isFetching}
+
isStale: {state.isStale}
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: data')) + await waitFor(() => expect(states.length).toBe(3)) - await sleep(100) - expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, @@ -3241,11 +3327,15 @@ describe('useQuery', () => { function Page() { const state = useQuery(key, async () => { - await sleep(1) + await sleep(10) return 'data' }) states.push(state) - return null + return ( +
+ {state.data}, {state.isStale}, {state.isFetching} +
+ ) } renderWithClient(queryClient, ) @@ -3384,21 +3474,30 @@ describe('useQuery', () => { // See https://github.com/tannerlinsley/react-query/issues/199 it('should use prefetched data for dependent query', async () => { const key = queryKey() + let count = 0 function Page() { const [enabled, setEnabled] = React.useState(false) const [isPrefetched, setPrefetched] = React.useState(false) - const query = useQuery(key, () => 'data', { - enabled, - }) + const query = useQuery( + key, + async () => { + count++ + await sleep(10) + return count + }, + { + enabled, + } + ) React.useEffect(() => { async function prefetch() { await queryClient.prefetchQuery(key, () => Promise.resolve('prefetched data') ) - setPrefetched(true) + act(() => setPrefetched(true)) } prefetch() }, []) @@ -3407,7 +3506,7 @@ describe('useQuery', () => {
{isPrefetched &&
isPrefetched
} -
{query.data}
+
data: {query.data}
) } @@ -3416,7 +3515,9 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('isPrefetched')) fireEvent.click(rendered.getByText('setKey')) - await waitFor(() => rendered.getByText('prefetched data')) + await waitFor(() => rendered.getByText('data: prefetched data')) + await waitFor(() => rendered.getByText('data: 1')) + expect(count).toBe(1) }) it('should support dependent queries via the enable config option', async () => { @@ -3665,15 +3766,24 @@ describe('useQuery', () => { const states: UseQueryResult[] = [] function Page() { - const queryInfo = useQuery(key, () => count++, { - refetchInterval: (data = 0) => (data < 2 ? 10 : false), - }) + const queryInfo = useQuery( + key, + async () => { + await sleep(10) + return count++ + }, + { + refetchInterval: (data = 0) => (data < 2 ? 10 : false), + } + ) states.push(queryInfo) return (

count: {queryInfo.data}

+

status: {queryInfo.status}

+

data: {queryInfo.data}

refetch: {queryInfo.isRefetching}

) @@ -4031,7 +4141,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2 expect(selectRun).toBe(2) - rendered.getByRole('button', { name: /inc/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3 expect(selectRun).toBe(3) @@ -4076,18 +4186,18 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2 - rendered.getByRole('button', { name: /inc/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3 - rendered.getByRole('button', { name: /forceUpdate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await waitFor(() => rendered.getByText('forceValue: 2')) // data should still be 3 after an independent re-render await waitFor(() => rendered.getByText('Data: selected 3')) }) - it('select should structually share data', async () => { + it('select should structurally share data', async () => { const key1 = queryKey() const states: Array> = [] @@ -4124,7 +4234,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Data: [2,3]')) expect(states).toHaveLength(1) - rendered.getByRole('button', { name: /forceUpdate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await waitFor(() => rendered.getByText('forceValue: 2')) await waitFor(() => rendered.getByText('Data: [2,3]')) @@ -4172,12 +4282,12 @@ describe('useQuery', () => { it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => { const key = queryKey() - const states: UseQueryResult[] = [] + const states: UseQueryResult[] = [] const queryFn: QueryFunction = async ctx => { const [, limit] = ctx.queryKey const value = limit % 2 && ctx.signal ? 'abort' : `data ${limit}` - await sleep(10) + await sleep(25) return value } @@ -4187,6 +4297,7 @@ describe('useQuery', () => { return (

Status: {state.status}

+

data: {state.data}

) } @@ -4202,9 +4313,9 @@ describe('useQuery', () => { ) await waitFor(() => rendered.getByText('off')) - await sleep(10) + await sleep(20) - expect(states).toHaveLength(4) + await waitFor(() => expect(states).toHaveLength(4)) expect(queryCache.find([key, 0])?.state).toMatchObject({ data: 'data 0', @@ -4307,7 +4418,7 @@ describe('useQuery', () => { const state = useQuery( key, async () => { - await sleep(1) + await sleep(10) count++ return count }, @@ -4316,20 +4427,26 @@ describe('useQuery', () => { states.push(state) - React.useEffect(() => { - setActTimeout(() => { - queryClient.resetQueries(key) - }, 10) - }, []) - - return null + return ( +
+ +
data: {state.data ?? 'null'}
+
isFetching: {state.isFetching}
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(100) + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /reset/i })) + + await waitFor(() => expect(states.length).toBe(4)) + + await waitFor(() => rendered.getByText('data: 2')) + + expect(count).toBe(2) - expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, isLoading: true, @@ -4368,7 +4485,8 @@ describe('useQuery', () => { function Page() { const state = useQuery( key, - () => { + async () => { + await sleep(10) count++ return count }, @@ -4379,23 +4497,28 @@ describe('useQuery', () => { const { refetch } = state - React.useEffect(() => { - setActTimeout(() => { - refetch() - }, 0) - setActTimeout(() => { - queryClient.resetQueries(key) - }, 50) - }, [refetch]) - - return null + return ( +
+ + +
data: {state.data ?? 'null'}
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(100) + await waitFor(() => rendered.getByText('data: null')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => rendered.getByText('data: 1')) + fireEvent.click(rendered.getByRole('button', { name: /reset/i })) + + await waitFor(() => rendered.getByText('data: null')) + await waitFor(() => expect(states.length).toBe(4)) + + expect(count).toBe(1) - expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, isLoading: true, @@ -4438,7 +4561,10 @@ describe('useQuery', () => { } function Page() { - renders++ + React.useEffect(() => { + renders++ + }) + useQuery(key, () => 'test', { queryKeyHashFn }) return null } @@ -4781,7 +4907,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('data: data1')) const onlineMock = mockNavigatorOnLine(false) - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText( @@ -4844,7 +4970,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('data: data1')) const onlineMock = mockNavigatorOnLine(false) - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused') @@ -4897,7 +5023,7 @@ describe('useQuery', () => { rendered.getByText('status: loading, fetchStatus: paused') ) - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) @@ -4951,7 +5077,7 @@ describe('useQuery', () => { expect(rendered.getByText('data: initial')).toBeInTheDocument() }) - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) @@ -5006,7 +5132,7 @@ describe('useQuery', () => { }) // triggers one pause - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) @@ -5136,7 +5262,7 @@ describe('useQuery', () => { rendered.getByText('status: loading, fetchStatus: paused') ) - rendered.getByRole('button', { name: /hide/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) @@ -5191,7 +5317,7 @@ describe('useQuery', () => { rendered.getByText('status: loading, fetchStatus: paused') ) - rendered.getByRole('button', { name: /cancel/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) await waitFor(() => rendered.getByText('status: loading, fetchStatus: idle') @@ -5261,13 +5387,15 @@ describe('useQuery', () => { const onlineMock = mockNavigatorOnLine(false) - rendered.getByRole('button', { name: /invalidate/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused') ) - rendered.getByRole('button', { name: /hide/i }).click() + fireEvent.click(rendered.getByRole('button', { name: /hide/i })) + + await sleep(15) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) @@ -5278,6 +5406,7 @@ describe('useQuery', () => { fetchStatus: 'idle', status: 'success', }) + expect(count).toBe(typeof AbortSignal === 'function' ? 1 : 2) onlineMock.mockRestore() diff --git a/src/reactjs/tests/utils.tsx b/src/reactjs/tests/utils.tsx index 95d94238ad..adfad740f9 100644 --- a/src/reactjs/tests/utils.tsx +++ b/src/reactjs/tests/utils.tsx @@ -63,7 +63,7 @@ export function sleep(timeout: number): Promise { } export function setActTimeout(fn: () => void, ms?: number) { - setTimeout(() => { + return setTimeout(() => { act(() => { fn() }) @@ -89,7 +89,7 @@ export const Blink: React.FC<{ duration: number }> = ({ React.useEffect(() => { setShouldShow(true) - const timeout = setTimeout(() => setShouldShow(false), duration) + const timeout = setActTimeout(() => setShouldShow(false), duration) return () => { clearTimeout(timeout) } diff --git a/src/reactjs/useBaseQuery.ts b/src/reactjs/useBaseQuery.ts index fe528bf00c..8726387657 100644 --- a/src/reactjs/useBaseQuery.ts +++ b/src/reactjs/useBaseQuery.ts @@ -1,8 +1,7 @@ import React from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' -import { QueryKey } from '../core' -import { notifyManager } from '../core/notifyManager' -import { QueryObserver } from '../core/queryObserver' +import { QueryKey, notifyManager, QueryObserver } from '../core' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { useQueryClient } from './QueryClientProvider' import { UseBaseQueryOptions } from './types' @@ -25,9 +24,6 @@ export function useBaseQuery< >, Observer: typeof QueryObserver ) { - const mountedRef = React.useRef(false) - const [, forceUpdate] = React.useState(0) - const queryClient = useQueryClient({ context: options.context }) const isHydrating = useIsHydrating() const errorResetBoundary = useQueryErrorResetBoundary() @@ -80,34 +76,23 @@ export function useBaseQuery< ) ) - let result = observer.getOptimisticResult(defaultedOptions) + const result = observer.getOptimisticResult(defaultedOptions) + + useSyncExternalStore( + React.useCallback( + onStoreChange => + isHydrating + ? () => undefined + : observer.subscribe(notifyManager.batchCalls(onStoreChange)), + [observer, isHydrating] + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult() + ) React.useEffect(() => { - mountedRef.current = true - - let unsubscribe: (() => void) | undefined - - if (!isHydrating) { - errorResetBoundary.clearReset() - - unsubscribe = observer.subscribe( - notifyManager.batchCalls(() => { - if (mountedRef.current) { - forceUpdate(x => x + 1) - } - }) - ) - - // Update result to make sure we did not miss any query updates - // between creating the observer and subscribing to it. - observer.updateResult() - } - - return () => { - mountedRef.current = false - unsubscribe?.() - } - }, [isHydrating, errorResetBoundary, observer]) + errorResetBoundary.clearReset() + }, [errorResetBoundary]) React.useEffect(() => { // Do not notify on updates because of changes in the options because @@ -149,9 +134,7 @@ export function useBaseQuery< } // Handle result property usage tracking - if (!defaultedOptions.notifyOnChangeProps) { - result = observer.trackResult(result) - } - - return result + return !defaultedOptions.notifyOnChangeProps + ? observer.trackResult(result) + : result } diff --git a/src/reactjs/useIsFetching.ts b/src/reactjs/useIsFetching.ts index 389ada5c38..2fe9554cb2 100644 --- a/src/reactjs/useIsFetching.ts +++ b/src/reactjs/useIsFetching.ts @@ -1,26 +1,13 @@ import React from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' -import { notifyManager } from '../core/notifyManager' -import { QueryKey } from '../core/types' -import { ContextOptions } from '../reactjs/types' +import { ContextOptions } from './types' +import { QueryKey, notifyManager } from '../core' import { parseFilterArgs, QueryFilters } from '../core/utils' -import { QueryClient } from '../core' import { useQueryClient } from './QueryClientProvider' interface Options extends ContextOptions {} -const checkIsFetching = ( - queryClient: QueryClient, - filters: QueryFilters, - isFetching: number, - setIsFetching: React.Dispatch> -) => { - const newIsFetching = queryClient.isFetching(filters) - if (isFetching !== newIsFetching) { - setIsFetching(newIsFetching) - } -} - export function useIsFetching(filters?: QueryFilters, options?: Options): number export function useIsFetching( queryKey?: QueryKey, @@ -32,49 +19,17 @@ export function useIsFetching( arg2?: QueryFilters | Options, arg3?: Options ): number { - const mountedRef = React.useRef(false) - const [filters, options = {}] = parseFilterArgs(arg1, arg2, arg3) - const queryClient = useQueryClient({ context: options.context }) - - const [isFetching, setIsFetching] = React.useState( - queryClient.isFetching(filters) + const queryCache = queryClient.getQueryCache() + + return useSyncExternalStore( + React.useCallback( + onStoreChange => + queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), + [queryCache] + ), + () => queryClient.isFetching(filters), + () => queryClient.isFetching(filters) ) - - const filtersRef = React.useRef(filters) - filtersRef.current = filters - const isFetchingRef = React.useRef(isFetching) - isFetchingRef.current = isFetching - - React.useEffect(() => { - mountedRef.current = true - - checkIsFetching( - queryClient, - filtersRef.current, - isFetchingRef.current, - setIsFetching - ) - - const unsubscribe = queryClient.getQueryCache().subscribe( - notifyManager.batchCalls(() => { - if (mountedRef.current) { - checkIsFetching( - queryClient, - filtersRef.current, - isFetchingRef.current, - setIsFetching - ) - } - }) - ) - - return () => { - mountedRef.current = false - unsubscribe() - } - }, [queryClient]) - - return isFetching } diff --git a/src/reactjs/useIsMutating.ts b/src/reactjs/useIsMutating.ts index 013525ba2c..51b477a8dd 100644 --- a/src/reactjs/useIsMutating.ts +++ b/src/reactjs/useIsMutating.ts @@ -1,4 +1,5 @@ import React from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' import { notifyManager } from '../core/notifyManager' import { MutationKey } from '../core/types' @@ -22,39 +23,18 @@ export function useIsMutating( arg2?: Omit | Options, arg3?: Options ): number { - const mountedRef = React.useRef(false) const [filters, options = {}] = parseMutationFilterArgs(arg1, arg2, arg3) const queryClient = useQueryClient({ context: options.context }) - - const [isMutating, setIsMutating] = React.useState( - queryClient.isMutating(filters) + const queryCache = queryClient.getQueryCache() + + return useSyncExternalStore( + React.useCallback( + onStoreChange => + queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), + [queryCache] + ), + () => queryClient.isMutating(filters), + () => queryClient.isMutating(filters) ) - - const filtersRef = React.useRef(filters) - filtersRef.current = filters - const isMutatingRef = React.useRef(isMutating) - isMutatingRef.current = isMutating - - React.useEffect(() => { - mountedRef.current = true - - const unsubscribe = queryClient.getMutationCache().subscribe( - notifyManager.batchCalls(() => { - if (mountedRef.current) { - const newIsMutating = queryClient.isMutating(filtersRef.current) - if (isMutatingRef.current !== newIsMutating) { - setIsMutating(newIsMutating) - } - } - }) - ) - - return () => { - mountedRef.current = false - unsubscribe() - } - }, [queryClient]) - - return isMutating } diff --git a/src/reactjs/useMutation.ts b/src/reactjs/useMutation.ts index 71c0744d40..4a10b87d02 100644 --- a/src/reactjs/useMutation.ts +++ b/src/reactjs/useMutation.ts @@ -1,6 +1,7 @@ import React from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' -import { notifyManager } from '../core/notifyManager' +import { notifyManager } from '../core' import { noop, parseMutationArgs } from '../core/utils' import { MutationObserver } from '../core/mutationObserver' import { useQueryClient } from './QueryClientProvider' @@ -74,54 +75,46 @@ export function useMutation< | UseMutationOptions, arg3?: UseMutationOptions ): UseMutationResult { - const mountedRef = React.useRef(false) - const [, forceUpdate] = React.useState(0) - const options = parseMutationArgs(arg1, arg2, arg3) const queryClient = useQueryClient({ context: options.context }) - const obsRef = React.useRef< - MutationObserver - >() - - if (!obsRef.current) { - obsRef.current = new MutationObserver(queryClient, options) - } else { - obsRef.current.setOptions(options) - } - - const currentResult = obsRef.current.getCurrentResult() + const [observer] = React.useState( + () => + new MutationObserver( + queryClient, + options + ) + ) React.useEffect(() => { - mountedRef.current = true + observer.setOptions(options) + }, [observer, options]) - const unsubscribe = obsRef.current!.subscribe( - notifyManager.batchCalls(() => { - if (mountedRef.current) { - forceUpdate(x => x + 1) - } - }) - ) - return () => { - mountedRef.current = false - unsubscribe() - } - }, []) + const result = useSyncExternalStore( + React.useCallback( + onStoreChange => + observer.subscribe(notifyManager.batchCalls(onStoreChange)), + [observer] + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult() + ) const mutate = React.useCallback< UseMutateFunction - >((variables, mutateOptions) => { - obsRef.current!.mutate(variables, mutateOptions).catch(noop) - }, []) + >( + (variables, mutateOptions) => { + observer.mutate(variables, mutateOptions).catch(noop) + }, + [observer] + ) if ( - currentResult.error && - shouldThrowError(obsRef.current.options.useErrorBoundary, [ - currentResult.error, - ]) + result.error && + shouldThrowError(observer.options.useErrorBoundary, [result.error]) ) { - throw currentResult.error + throw result.error } - return { ...currentResult, mutate, mutateAsync: currentResult.mutate } + return { ...result, mutate, mutateAsync: result.mutate } } diff --git a/src/reactjs/useQueries.ts b/src/reactjs/useQueries.ts index 9fa923eede..95a29d1775 100644 --- a/src/reactjs/useQueries.ts +++ b/src/reactjs/useQueries.ts @@ -1,6 +1,7 @@ import React from 'react' -import { QueryKey, QueryFunction } from '../core/types' +import { useSyncExternalStore } from 'use-sync-external-store/shim' +import { QueryKey, QueryFunction } from '../core/types' import { notifyManager } from '../core/notifyManager' import { QueriesObserver } from '../core/queriesObserver' import { useQueryClient } from './QueryClientProvider' @@ -142,9 +143,6 @@ export function useQueries({ queries: readonly [...QueriesOptions] context?: UseQueryOptions['context'] }): QueriesResults { - const mountedRef = React.useRef(false) - const [, forceUpdate] = React.useState(0) - const queryClient = useQueryClient({ context }) const isHydrating = useIsHydrating() @@ -169,26 +167,17 @@ export function useQueries({ const result = observer.getOptimisticResult(defaultedQueries) - React.useEffect(() => { - mountedRef.current = true - - let unsubscribe: (() => void) | undefined - - if (!isHydrating) { - unsubscribe = observer.subscribe( - notifyManager.batchCalls(() => { - if (mountedRef.current) { - forceUpdate(x => x + 1) - } - }) - ) - } - - return () => { - mountedRef.current = false - unsubscribe?.() - } - }, [isHydrating, observer]) + useSyncExternalStore( + React.useCallback( + onStoreChange => + isHydrating + ? () => undefined + : observer.subscribe(notifyManager.batchCalls(onStoreChange)), + [observer, isHydrating] + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult() + ) React.useEffect(() => { // Do not notify on updates because of changes in the options because diff --git a/yarn.lock b/yarn.lock index 4d817e5864..c1df155350 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2225,7 +2225,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3": +"@babel/runtime@^7.10.2": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== @@ -2874,15 +2874,33 @@ "@svgr/plugin-svgo" "^6.1.0" rollup-pluginutils "^2.8.2" -"@testing-library/dom@^7.17.1": - version "7.18.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.18.1.tgz#c49530410fb184522b3b59c4f9cd6397dc5b462d" - integrity sha512-tGq4KAFjaI7j375sMM1RRVleWA0viJWs/w69B+nyDkqYLNkhdTHdV6mGkspJlkn3PUfyBDi3rERDv4PA/LrpVA== +"@testing-library/dom@^8.0.0": + version "8.11.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.4.tgz#dc94d830b862e7a20686b0379eefd931baf0445b" + integrity sha512-7vZ6ZoBEbr6bfEM89W1nzl0vHbuI0g0kRrI0hwSXH3epnuqGO3KulFLQCKfmmW+60t7e4sevAkJPASSMmnNCRw== dependencies: - "@babel/runtime" "^7.10.3" - aria-query "^4.2.2" - dom-accessibility-api "^0.4.5" - pretty-format "^25.5.0" + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/dom@^8.5.0": + version "8.11.3" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.3.tgz#38fd63cbfe14557021e88982d931e33fb7c1a808" + integrity sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" "@testing-library/jest-dom@^5.14.1": version "5.14.1" @@ -2899,19 +2917,34 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^10.4.7": - version "10.4.7" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.7.tgz#fc14847fb70a5e93576b8f7f0d1490ead02a9061" - integrity sha512-hUYbum3X2f1ZKusKfPaooKNYqE/GtPiQ+D2HJaJ4pkxeNJQFVUEvAvEh9+3QuLdBeTWkDMNY5NSijc5+pGdM4Q== +"@testing-library/react-17@npm:@testing-library/react@^12.1.4": + version "12.1.4" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.4.tgz#09674b117e550af713db3f4ec4c0942aa8bbf2c0" + integrity sha512-jiPKOm7vyUw311Hn/HlNQ9P8/lHNtArAx0PisXyFixDDvfl8DbD6EUdbshK5eqauvBSvzZd19itqQ9j3nferJA== dependencies: - "@babel/runtime" "^7.10.3" - "@testing-library/dom" "^7.17.1" + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "*" + +"@testing-library/react@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.0.0.tgz#8cdaf4667c6c2b082eb0513731551e9db784e8bc" + integrity sha512-p0lYA1M7uoEmk2LnCbZLGmHJHyH59sAaZVXChTXlyhV/PRW9LoIh4mdf7tiXsO8BoNG+vN8UnFJff1hbZeXv+w== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.5.0" + "@types/react-dom" "*" "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.1.7": version "7.1.8" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.8.tgz#057f725aca3641f49fc11c7a87a9de5ec588a5d7" @@ -3028,6 +3061,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== +"@types/node@^16.11.10": + version "16.11.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.10.tgz#2e3ad0a680d96367103d3e670d41c2fed3da61ae" + integrity sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -3053,14 +3091,21 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/react-dom@^16.9.8": +"@types/react-dom@*", "@types/react-dom@^16.9.8": version "16.9.14" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.14.tgz#674b8f116645fe5266b40b525777fc6bb8eb3bcd" integrity sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A== dependencies: "@types/react" "^16" -"@types/react@^16", "@types/react@^16.9.41": +"@types/react-dom@^17.0.11": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16", "@types/react@^16.9.41": version "16.14.15" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.15.tgz#95d8fa3148050e94bcdc5751447921adbe19f9e6" integrity sha512-jOxlBV9RGZhphdeqJTCv35VZOkjY+XIEY2owwSk84BNDdDv2xS6Csj6fhi+B/q30SR9Tz8lDNt/F2Z5RF3TrRg== @@ -3069,6 +3114,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17.0.37": + version "17.0.37" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" + integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -3093,6 +3147,11 @@ dependencies: "@types/jest" "*" +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -3319,6 +3378,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -3797,7 +3861,7 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: +chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4303,16 +4367,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" - integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== - dom-accessibility-api@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz#8c2aa6325968f2933160a0b7dbb380893ddf3e7d" integrity sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA== +dom-accessibility-api@^0.5.9: + version "0.5.11" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz#79d5846c4f90eba3e617d9031e921de9324f84ed" + integrity sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw== + dom-serializer@^1.0.1: version "1.3.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" @@ -6494,6 +6558,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + magic-string@0.25.7, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -7213,6 +7282,15 @@ pretty-format@^27.0.0, pretty-format@^27.0.6: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -7231,7 +7309,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7270,15 +7348,30 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-dom@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== +"react-17@npm:react@^17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + +"react-dom-17@npm:react-dom@^17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-dom@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" + integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.21.0" react-error-boundary@^2.2.2: version "2.2.2" @@ -7302,14 +7395,12 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react@^16.13.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== +react@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" + integrity sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" read-pkg-up@^2.0.0: version "2.0.0" @@ -7835,14 +7926,21 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" + integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== + dependencies: + loose-envify "^1.1.0" + "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -8663,6 +8761,11 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +use-sync-external-store@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0.tgz#d98f4a9c2e73d0f958e7e2d2c2bfb5f618cbd8fd" + integrity sha512-AFVsxg5GkFg8GDcxnl+Z0lMAz9rE8DGJCc28qnBuQF7lac57B5smLcT37aXpXIIPz75rW4g3eXHPjhHwdGskOw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"