From 4d753c01c3fa3bb0fb7b6e8935e2a656763b2811 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Sat, 2 Apr 2022 13:42:11 +0200 Subject: [PATCH] feat: add support for react 18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(react): react-18 update react dependencies and add use-sync-external-store polyfill * feat(core): react-18 use a version of uSES that actually has an implementation other than "Not Yet Implemented" * feat(core): react-18 looks like we also need the experimental version of react, because v18-alpha doesn't support uSES yet. * feat(core): react-18 update testing-library to v13 alpha * feat(core): react-18 do not update currentResult when it is equal to the previousResult, because we use the currentResult as snapshot for uSES, so it must be as stable as possible * feat(core): react-18 switch forceUpdate with uSES. - I'm not sure if the `updateResult` effect is still necessary, or if it's guaranteed that we can't miss any updates because we don't subscribe in an effect anymore; tests behave the same with / without it - tbd - subscribe must be stable, or else we wind up in an infinite loop. in order to be able to pass `observer.subscribe`, we must bind the function in the constructor * feat(core): react-18 make the first test a bit more stable - we don't want more than 2 results * feat(core): react-18 fix type issues in devtools tests, so we adhere to the new typings of testing-library v13 * feat(core): react-18 make devtools test more resilient: act throws an error in the latest version if used liked that, but we don't need it. We can just click the button and use waitFor, as documented here: https://testing-library.com/docs/guide-disappearance#2-using-waitfor * feat(core): react-18 don't re-assign result * feat(core): react-18 bring back the optimistic result; this is debatable because it means we actually _ignore_ whe result returned by uSES, but it makes for fewer re-renders as we can go back to silently update from the effect * feat(core): react-18 useIsFetching to uSES I don't fully understand the test that needed adaption, but the new numbers actually look more correct. The first thing that happens is showing the SecondQuery (after 50ms), and at that time, the FirstQuery is already fetching, so why should there be two zeros in the result array ... judging from the console mock assertion, we are testing if state hasn't been updated on an unmounted component, which now can't happen anymore with uSES, so we can remove it * feat(core): react-18 useIsMutatating to uSES As a positive side-effect, there seem to be fewer re-renders now - the new numbers in the tests do make sense * feat(core): react-18 useMutation to uSES one big change is moving `setOptions` into a useEffect - similar to what `useQuery` is doing. However, we have no `getOptimisticResult` in useMutation, so we'll have to see how this behaves the tests need some love - it's generally working, but the way the tests are written, we're getting some failure. * feat(core): react-18 wait for heading to to to value `3` before asserting the onSuccess / onSettled calls * feat(core): react-18 rewrite test to getByRole * feat(core): react-18 since we're not returning anything from onError or onSettled in the tests, the mutation updates the data on the screen before the callbacks have finished running, which is why the test needs to waitFor the callbacks to have been called * feat(core): react-18 work around console error from uSES by moving the console mock to the client part and / or increasing the assertion count for now * feat(core): react-18 there seems to be one less rendering, likely because of batching, getting rid of one render that has the same assertions as the previous state, which is nice * feat(core): react-18 update shim * feat(core): react-18 update to v18 alpha, which should had the native uSES impl * feat(core): react-18 bump uSES * feat(core): react-18 count renders correctly by incrementing the count in useEffect * feat(core): react-18 bump everything and import from /shim * feat(core): react-18 make test more resilient by not using fireEvent * feat(core): react-18 use findByText for more resilient tests * feat(core): react-18 test against react 17 and react 18 * feat(core): react-18 only run bundlewatch once * feat(core): react-18 give a better name * feat(core): react-18 useQueries to uSES * feat(core): react-18 really upgrade react (with exact versions, because alphas) * feat(core): react-18 remove version logging * feat(core): react-18 remove fixed version in test:ci script * feat(core): react-18 try to get rid of warning in suspense test * feat(core): react-18 remove wrongful mock assertion - uSES should not console.error anymore * feat(core): react-18 add missing server-side snapshots * feat(core): react-18 fix build, error is: [!] Error: 'useSyncExternalStore' is not exported by node_modules/use-sync-external-store/shim/index.js, imported by src/react/useIsFetching.ts https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module * feat(hydration): remove hydration package (#2936) * V4: streamline cancel refetch (#2937) * feat: streamline cancelRefetch the following functions now default to true for cancelRefetch: - refetchQueries (+invalidateQueries, + resetQueries) - query.refetch - fetchNextPage (unchanged) - fetchPreviousPage (unchanged) * feat: streamline cancelRefetch make sure that refetchOnReconnect and refetchOnWindowFocus do not cancel already running requests * feat: streamline cancelRefetch update tests refetch and invalidate now both cancel previous queries, which is intended, so we get more calls to the queryFn in these cases * feat: streamline cancelRefetch add more tests for cancelRefetch behavior * feat: streamline cancelRefetch update docs and migration guide * feat: streamline cancelRefetch simplify conditions by moving the ?? true default down to fetch on observer level; all 3 callers (fetchNextPage, fetchPreviousPage and refetch) just pass their options down and adhere to this default; refetch also only has 3 callers: - refetch from useQuery, where we want the default - onOnline and onFocus, where we now explicitly pass false to keep the previous behavior and add more tests * feat: streamline cancelRefetch we always call this.fetch() with options, so we can just as well make the mandatory also, streamline signatures by destructing values that can't be forwarded (and use empty object as default value) in options and just spread the rest * feat: streamline cancelRefetch fix types for refetch it was accidentally made too wide and allowed all refetchFilters, like `predicate`; but with `refetch` on an obserserver, there is nothing to filter for, except the page, so that is what we need to accept via `RefetchPageFilters` * feat: streamline cancelRefetch refetch never took a queryKey as param - it is always bound to the observer * feat: better query filters (#2938) * feat(core): react-18 bump dependencies to beta * feat(core): react-18 fix assertions about special react markup * feat(core): react-18 try to make ssr hydration tests work, but skip them for now * feat(core): react-18 bring back batching. for this to work, we need to batch the actual calls to `onStoreChange` received from uSES, and scheduleMicroTask also needs to defer one tick with setTimeout (couldn't find a better way) * feat(core): react-18 reduce timeout to make test less flaky * feat(core): react-18 fix useIsMutating tests: The same value can't really appear twice in the array because re-renders are batched; this seems like a nice improvement due to uSES * feat(core): react-18 make tests slower; it seems that batching together with a sleep(0) actually batches fast responses together, so we now go directly from idle to success * fix: rename react directory to reactjs (#2884) * fix: rename react directory to reactjs the directory being named "react" causes an error with the moduleDirectories option from jest * fix: update package.json files to match the updated reactjs directory name * fix: change react test utils imports to match new directory name * docs(v4): add renamed reactjs details to migration guide Co-authored-by: Eddy Vinck * feat: mutation cache duration (#2963) * feat: mutation cachetime stramline queryCache / mutationCache events by combining them into notifiable.ts * feat: mutation cachetime removable * feat: mutation cachetime add gc to mutations * feat: mutation cachetime streamline event types between queries and mutations * feat: mutation cachetime tests, and I forgot to implement optionalRemove, so make it abstract * feat: mutation cachetime replicate gc behavior from https://github.com/tannerlinsley/react-query/pull/2950 and add more tests * feat: mutation cachetime get test coverage back to 100% * feat: mutation cachetime docs * feat: mutation cachetime try to make tests more resilient * feat: mutation cachetime fix imports after merge conflict * feat(core): react-18 make ssr tests work in react 18: the updated fetch count is actually correct, as we always fetch once on the server, then sometimes another time on the client. see also this discussion: https://github.com/TkDodo/react-query/pull/2/files#r751305071 * feat(core): react-18 try to make tests work in 17 and 18 because of the different batching, we sometimes get different results, which we can avoid by making the tests really async and do some data fetching that takes at least some time, and write the tests more resilient * feat(core): react-18 resilient tests * refactor(persistQueryClient): Make persistQueryClient stable (#2961) * :truck: Remove experimental from persist-query-client * :truck: Rename persistor -> persister * ✏️ Fix Persistor -> Persister in imports * :truck: Update name in rollup config * :truck: Move createAsyncStoragePersister and createWebStoragePersister to stable version and rename persistor -> persister * 📝 Update documentation * 📝 Add migrating to v4 docs * Apply suggestions from code review Co-authored-by: Dominik Dorfmeister * feat(core): react-18 continue to stabilize tests * 2964 changes to on success callback (#2969) * feat(useQuery): onSuccess callback do not call onSuccess if update was done manually from setQueryData * feat(useQuery): onSuccess callback test that onSuccess is not called when setQueryData is used * feat(useQuery): onSuccess callback docs changes * feat(useQuery): onSuccess callback options spread is wrong - `updatedAt` is actually `dataUpdatedAt`. Oddly we didn't have a test, so I added one * 2919 query key array (#2988) * feat: query key array remove code that internally ensures that we get an Array, because it is now the expected interface, ensured by TypeScript * feat: query key array update tests to the new syntax * feat: query key array fix assertions, because there is no array wrapping happening internally anymore. The key you receive from the context is exactly the key you passed in * feat: query key array this test doesn't make much sense anymore * feat: query key array wrapping in an extra array doesn't yield the same results anymore since v4 because keys need to be an array * feat: query key array make docs adhere to new array key syntax * feat: query key array migration docs * feat(QueryObserver): track queries as default (#2987) * feat(Query Options): remove notifyOnChangePropsExclusion - remove related code from queryObserver - remove type def - remove related tests * docs(Query Options): update notifyOnChangePropsExclusion sections - remove from api references - add to v4 migration guide * feat(QueryObserver): "tracked" as default behavior - remove "tracked" completely if notifyOnChangeProps is not defined, behave as v3 "tracked" - add `notifyOnChangeProps: 'all' to opt out of the smart tracking TODO: Now that default behavior has changed, work out the failed tests. Which parts to change for current ones and possibly write new ones. * test(useQuery): adjust tests to pass for notifyOnChangeProps udpate * test(useInfiniteQuery): adjust tests to pass for notifyOnChangeProps udpate * test(QueryResetErrorBoundary): adjust tests to pass for notifyOnChangeProps udpate * refactor(QueryObserver): use nullish coalescing operator much cleaner than the negated if I started with * test(QueryResetErrorBoundary): remove "tracked" from test * revert: test(QueryResetErrorBoundary): adjust tests to pass for notifyOnChaneProps udpate This reverts commit a34b4720675dad5ee6ebde401639f328c0c83122. The changes are not necessary after PR #2993 fix. * refactor(QueryObserver): combine prop checks * docs(notifyOnChangeProps): update docs to reflect new api * refactor: Remove deprecated promise cancel (#2996) * :fire: Remove the cancel method on promise for cancelling promise * ✅ Fix query client tests * ✅ Update query and useQuery tests * ✅ Update use infinite query tests * 📝 Update migartion guide * :bug: Fix linking in documentation * :pencil: Fix grammatical errors in docs Co-authored-by: Dominik Dorfmeister * :refactor: Use abortSignal for query cancellation in InfiniteQueryBehavior * 🚨 Fix lint errors * :recycle: Move define signal property to a separate function Co-authored-by: Dominik Dorfmeister * remove test that doesn't make sense anymore - we don't allow different falsy query keys now * feat(core): react-18 re-add missing import after merge conflicts * feat(core): react-18 we need to observe isFetching to get a re-render with it thanks to tracked queries being on per default now * feat(core): react-18 observe fields we are actually checking * feat(core): react-18 stabilize tests * feat(core): react-18 make one specific test assert differently for react17/18 because batching with uSES works slightly differently I think * feat(core): react-18 remove now unnecessary useEffect: uSES makes sure that we can't miss any query updates between creating the observer and subscribing to it, even if the shim is used * feat(core): react-18 stabilize useQueries test: since both queries have the same timeout, it seems that the initial updates are now batched, leading to 1 less re-render * feat(core): react-18 stabilize useQueries test: a little less rerenders * feat(core): react-18 stabilize useInfiniteQuery tests * feat(core): react-18 use setActTimeout in Blink to avoid warning in react17 * feat(core): react-18 nodejs types conflict :/ * feat(core): react-18 always schedule garbage collection instead of removing directly with cacheTime 0, as callbacks might not fire otherwise * ✅ Use getByLabelText for opening query details * :bug: Use findBy* instead of getBy* * feat(core): react-18 wait for button to appear before clicking it * feat(core): react-18 seems like a broke a cacheTime: 0 test with the latest changes that I can't fix even with making the test more stable, so I'm partially reverting that change. However, we really shouldn't call `remove` directly, but always `optionalRemove` to never remove fetching queries. * feat(core): react-18 okay, lets go back to always instantly removing if we have 0 cacheTime. Not sure why the optionalRemove breaks a test * feat(core): react-18 stabilize flaky test - some renders seem to be batched if we don't sleep enough * feat(core): react-18 adapt scheduleMicroTask: we don't need to defer error throwing anymore now that we always defer with sleep(0) initially, which means the test only needs to assure that the callback is not invoked immediately, but deferred by one Promise "tick" * feat(core): react-18 fix suspense tests: since scheduleMicroTask now always defers by a promise tick (setTimeout(0)), and callbacks are batched into this, it takes one tick longer until the callbacks are invoked; we can "fix" that with a sleep(0) in the tests, or just by using waitFor to wait until the callbacks have been called * ✅ Make sorting test more robust * ✅ Make queries dependent on each other * 2927 offline queries (#3006) * feat(useQuery): offline queries remove defaultQueryObserverOptions because it is the same as defaultQueryOptions and we can just use that * feat(useQuery): offline queries setup dependent default values, to make it easier to work with them * feat(useQuery): offline queries basic changes to retryer: - pause the query before fetching depending upon networkMode - pause retries depending upon networkRetry * feat(useQuery): offline queries move networkRetry and networkMode defaults to the retryer creation, because we need the same for mutations * feat(useQuery): offline queries decouple focus and online manager: we're now informing caches of a focus when we're focussed, and about an online event if we come online; if the retryer continues, it can then decide to not fetch depending on our networkMode * feat(useQuery): offline queries expose isPaused on the queryResult and make sure isFetching is false when we are paused * feat(useQuery): offline queries knowing if we can fetch depends on if we are paused or not, as other conditions should apply also, rename options (not sure if that will stick though) * feat(useQuery): offline queries adjust existing tests for isPaused being exposed * feat(useQuery): offline queries fix existing test by setting options to emulate the previous behaviour, otherwise, with `mockNavigatorOnline` being set to false right from the start, the mutation would never fire off * feat(useQuery): offline queries adapt onOnline, onFocus tests to new behavior: they are now decoupled, and onOnline is always called even when not focused and vice versa. The retryer should make sure to not continue fetching if necessary * feat(useQuery): offline queries first test for networkMode * feat(useQuery): offline queries isFetching and isPaused are now derived and stored together in a fetchingState enum (idle, fetching, paused) * feat(useQuery): offline queries better networkMode api: online, always, offlineFirst (basically always but with paused retries) * feat(useQuery): offline queries more tests for networkMode: online * feat(useQuery): offline queries more tests for networkMode: online * feat(useQuery): offline queries tests for networkMode: always * feat(useQuery): offline queries fix tests that were influencing each other by using proper jest mocks for online and visibility state * add paused queries to the devtools.tsx * feat(useQuery): offline queries never stop pausing when continueFn is called. Initially, I only had this guard for when it's called from the outside, e.g. for onWindowFocus while still being offline, but we need this always because otherwise query cancellation can potentially continue a paused query * feat(useQuery): offline queries okay, pausing multiple times was a bad idea, continueFn() will be called eventually anyways * feat(useQuery): offline queries attempt at offline toggle button * feat(useQuery): offline queries different icons, padding, color * feat(useQuery): offline queries i messed up the icon order * feat(useQuery): offline queries guard against illegal state transitions: paused queries can unmount or get cancelled, in which case we shouldn't continue them, even if we dispatch the continue event * feat(useQuery): offline queries fix devtools tests, account for paused queries * Revert "feat(useQuery): offline queries" This reverts commit a647f64a051ca4c02a872e7871b4b2ce49aeda2c. * feat(useQuery): offline queries keep the do-not-start logic out of the run function, and thus out of promiseOrValue. if the promise has already been resolved in the meantime, e.g. because of a `cancel`, the run method will just do nothing, while the previous logic would've started to fetch * feat(useQuery): offline queries show inactive as higher priority than paused * feat(useQuery): offline queries make sure that optimistic results don't show an intermediate fetching state, but go opmistically to paused instead * feat(useQuery): offline queries real result needs to match optimistic result * feat(useQuery): offline queries stupid mistake * feat(useQuery): offline queries keep status color and status label in sync * feat(useQuery): offline queries make networkMode param mandatory for canFetch (and default to online internally) so that we can't screw that up again * feat(useQuery): offline queries make sure test "finishes" to avoid prints to the console if another test goes online again * feat(useQuery): offline queries move cancel function to the top, as it's no longer dependent on the promise since the `.cancel` function is gone; all we need is to abort the signal and reject the promise of the retryer * feat(useQuery): offline queries inline canContinue, because it's now only called if the query is in paused state anyways * feat(useQuery): offline queries avoid the impossible state by not calling config.onContinue for already resolved queries, as that would put them right into fetching state again, without actually fetching * feat(useQuery): offline queries let resolved querie continue, but don't put them in fetching state * feat(useQuery): offline queries fix merge conflict and invert condition because no-negated-condition * feat(useQuery): offline queries add test for abort signal consumed - different results expected for node < 15 where we don't have AbortController, thus can't consume the signal * feat(useQuery): offline queries online queries should not fetch if paused and we go online when cancelled and no refetchOnReconnect * feat(useQuery): offline queries gc test * feat(useQuery): offline queries offlineFirst test * feat(useQuery): offline queries mock useMediaQuery to get rid of unnecessary check in devtools - if window is defined, `matchMedia` is also defined * feat(useQuery): offline queries use a higher retryDelay to make test more stable, otherwise, it might start retrying before we "go offline" * feat(useQuery): offline queries improve devtools test: check if onClick props are being called * feat(useQuery): offline queries add devtools test for offline mock * feat(useQuery): offline queries offline mutations test * feat(useQuery): offline queries network mode docs (unfinished) * feat(useQuery): offline queries network mode docs * feat(useQuery): offline queries fix merge conflicts * feat(core): react-18 fix new devtools test * feat(core): react-18 stabilize flaky test * feat(core): react-18 stabilize flaky test * refactor(queryClient): remove undocumented methods * feat(core): react-18 stabilize test: make sure we wait until we have really removed the data before we go online * feat(core): react-18 stabilize test with a sleep ¯\_(ツ)_/¯ * feat(core): react-18 add a dedicated script to test against react v17 * feat(core): react-18 queries need more "distance" between them to not be batched together and so that we get the expected render results in v17 and v18. if queries return faster, results will be batched, resulting in fewer re-renders, which is also good * feat(core): react-18 remove test about "unmounted" warning because we now uSES * feat(core): react-18 wrap update from inside useEffect in act, and make sure the queryFn doesn't return undefined * feat(core): react-18 devtools to uSES; since we have no stable snapshot - Object.values is always a new array, and queryCache.getAll() only changes if we add or remove things - we use the ref to let the getSnapshot function create a new array every time we have received an update from the subscription to trigger a re-render * feat(core): react-18 make devtools work in 18, but that brings back the act warnings in 17 * feat(core): react-18 stabilize another test * feat(core): react-18 stabilize tests: make sure that we wait for the test to finish to get rid of the act warning in 17 * feat(core): react-18 disable 3 ssr-hydration tests in react17 env. they use the shim, which uses layout effect under the hood if window is available, which doesn't work when rendering on the server; I've tried a lot to "mock" the correct environment, but to no success; the test will still run fine under react18 env, so I think we're good here * feat(core): react-18 flaky test much * feat(core): react-18 fix flaky test: we actually expect a length of two here, because suspense takes care of the intermediate loading state * feat(core): react-18 slowdown test some more to decrease flakyness * feat(core): react-18 simplify keepPreviousData tests. we are asserting all intermediate data steps anyways, and we don't really care about the intermediate fetching steps. exact batching happens depending on speed of the test / shim being used or not etc. Sometimes, intermediate steps are batched together into one re-render, which is not a bad thing. We would have to make the tests really slow to be super predictable though * fix: offline mutations fixes (#3051) * feat: offline mutations move reducer into Mutation class to avoid passing state (and options) around * feat: offline mutations optimistically set paused state depending on if we can fetch or not to avoid an intermediate state where we are loading but not paused * examples: fix query keys in basic examples because we need those for preview builds * fix(useMutation): make sure cacheCallbacks are always called even if the useMutation component unmounts and we have a cacheTime of 0; the fix was cherry-picked from the react-18 branch, where we also introduced this behavior * feat(core): react-18 there seems to be one less re-render, consistently, with this test * Feature/cachetime zero (#3054) * refactor: cacheTime-zero remove special handling for cacheTime: 0 and schedule a normal garbage collection for those queries. They will be eligible for gc after a setTimeout(0), but then they will only be optionally removed. This makes sure that paused queries are NOT gc'ed * refactor: cacheTime-zero remove special test "about online queries with cacheTime:0 should not fetch if paused and then unmounted". paused queries will now be kept until they continue, just like with every other query, unless query cancellation or abort signal was involved * refactor: cacheTime-zero adapt "remounting" test: if the same query with cacheTime 0 unmounts and remounts in the same cycle, the query will now be picked up and will not go to loading state again. I think this is okay * refactor: cacheTime-zero re-add instant query removal after fetching, because fetching via `queryClient.fetchQuery` will not remove the query otherwise, because the normal gc-mechanism now checks for `hadObservers` due to a suspense issue :/ * refactor: cacheTime-zero weird edge case: the previous logic was instantly removing the query _while_ it was still fetching, which is something we likely don't want. The data will stay in the currentQuery of the observer if the observer unsubscribes but still exists, and a new subscription will pick it up, unless the query was explicitly cancelled or the abort signal was consumed. * refactor: cacheTime-zero we need to wait a tick because even cacheTime 0 now waits at least a setTimeout(0) to be eligible for gc * refactor: cacheTime-zero schedule a new garbage collection after each new fetch; this won't do anything when you still have observers, but it fixes an edge case where prefetching took longer than the cacheTime, in which case the query was again never removed test needed adaption because we don't instantly remove, but deferred by a tick * refactor: cacheTime-zero stabilize test * refactor: cacheTime-zero apply a different suspense "workaround": do not garbage collect when fetching optimistically (done only by suspense) - gc will kick in once an observer subscribes; this will make sure we can still gc other fetches that don't have an observer consistently, like prefetching when the fetch takes longer than the gc time (which was leaking with the old workaround) * refactor: cacheTime-zero remove leftover * refactor: cacheTime-zero since every fetch triggers a new gc cycle, we don't need to do this in a loop anymore also, reset isFetchingOptimistic after every fetch * add publishing capabilities for alpha branch * feat(core): react-18 fix merge conflict * feat(core): react-18 remove duplicate test (introduced in merge conflict) * feat(core): react-18 flaky test * feat(core): react-18 try to make flaky test more stable * :recycle: Refactor devtools subscriptions * :sparkles: Move query states subscriptions to separate component * :recycle: Move active query to a separate component * feat(core): react-18 update to react-18 rc * feat(core): react-18 get rid of unused ts-expect-error, as we now have access to process.env * feat(core): react-18 tests: switch from .click() to fireEvent.click to avoid act warnings * feat(core): react-18 tests: fix suspense act warning * feat(core): react-18 use fireEvent.click() for tests because of act warnings * fix merge conflicts * more merge conflicts * another merge conflict: idle state doesn't exist anymore * fix tests, we need to check for fetchStatus now * remove unnecessary optional chaining in tests * prettier * useIsMutating: fix assertions - no more duplicates in react18 * also, no duplicate renderings for PersistQueryClient anymore * make tests more resilient don't wait for a specific time and then get, just try to find the text at all (timing with react18 can be different) * another flaky one * re-write new custom context tests to be the same as other tests * re-write new custom context tests to be the same as other tests * remove calls to getServerSnapshot it's optional and we don't do anything meaningful as of now in it; will need to re-add it once we do react18 hydration * Revert "remove calls to getServerSnapshot" This reverts commit eabcd4667523fbe44497a5e56815c9aa9b894f6a. * update to latest alphas an rcs * use testing-library 12 for react-17 test suite because the latest alpha dropped support for react17 * update from release candidates to actual releases * make another test more stable Co-authored-by: Eddy Co-authored-by: Eddy Vinck Co-authored-by: Prateek Surana Co-authored-by: Rene Dellefont --- .github/workflows/test-and-publish.yml | 7 +- jest.setup.js | 30 + package.json | 21 +- rollup.config.js | 11 +- src/core/queryObserver.ts | 8 +- src/core/subscribable.ts | 1 + src/core/tests/queryClient.test.tsx | 6 +- src/core/tests/utils.test.tsx | 34 +- src/core/utils.ts | 10 +- src/devtools/devtools.tsx | 792 ++++++++++-------- src/devtools/tests/devtools.test.tsx | 93 +- src/devtools/tests/utils.tsx | 14 +- src/devtools/utils.ts | 57 +- .../tests/PersistQueryClientProvider.test.tsx | 8 +- src/reactjs/tests/Hydrate.test.tsx | 16 +- .../tests/QueryResetErrorBoundary.test.tsx | 2 +- src/reactjs/tests/ssr-hydration.test.tsx | 80 +- src/reactjs/tests/suspense.test.tsx | 56 +- src/reactjs/tests/useInfiniteQuery.test.tsx | 194 +++-- src/reactjs/tests/useIsFetching.test.tsx | 76 +- src/reactjs/tests/useIsMutating.test.tsx | 8 +- src/reactjs/tests/useMutation.test.tsx | 159 ++-- src/reactjs/tests/useQueries.test.tsx | 239 ++---- src/reactjs/tests/useQuery.test.tsx | 479 +++++++---- src/reactjs/tests/utils.tsx | 4 +- src/reactjs/useBaseQuery.ts | 57 +- src/reactjs/useIsFetching.ts | 71 +- src/reactjs/useIsMutating.ts | 42 +- src/reactjs/useMutation.ts | 67 +- src/reactjs/useQueries.ts | 37 +- yarn.lock | 183 +++- 31 files changed, 1563 insertions(+), 1299 deletions(-) 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"