snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
517
packages/vue-query/CHANGELOG.md
Normal file
517
packages/vue-query/CHANGELOG.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# @tanstack/vue-query
|
||||
|
||||
## 5.102.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.8
|
||||
|
||||
## 5.102.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.7
|
||||
|
||||
## 5.102.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.6
|
||||
|
||||
## 5.102.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`578e5c2`](https://github.com/TanStack/query/commit/578e5c26e8ebd0d7351b4b8e2bafba695e672b8d)]:
|
||||
- @tanstack/query-core@5.102.5
|
||||
|
||||
## 5.102.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a05df6a`](https://github.com/TanStack/query/commit/a05df6aefb0e2489ec2c879ae16e2ee7cb3123ec)]:
|
||||
- @tanstack/query-core@5.102.4
|
||||
|
||||
## 5.102.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.3
|
||||
|
||||
## 5.102.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`80fbf73`](https://github.com/TanStack/query/commit/80fbf73e77892d702c107e14a84c219a8ed825dc)]:
|
||||
- @tanstack/query-core@5.102.2
|
||||
|
||||
## 5.102.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`134890d`](https://github.com/TanStack/query/commit/134890dbdc60e4fb0313e44b512d29284ca82f96)]:
|
||||
- @tanstack/query-core@5.102.1
|
||||
|
||||
## 5.102.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#11208](https://github.com/TanStack/query/pull/11208) [`97ce30b`](https://github.com/TanStack/query/commit/97ce30b51e3040752f3f4172e9fd11be4edea774) - add new imperitive methods to QueryClient proxy
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10373](https://github.com/TanStack/query/pull/10373) [`6e3d521`](https://github.com/TanStack/query/commit/6e3d521fe54f78624e53c1c6f9cadd678504bee8) - fix(types): propagate generic type parameters to `useMutationState` select callback
|
||||
|
||||
- [#11224](https://github.com/TanStack/query/pull/11224) [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b) - Fix `queryOptions` and `infiniteQueryOptions` return types so exported inferred options can be emitted in declaration files without leaking internal data tag symbols.
|
||||
|
||||
- [#8737](https://github.com/TanStack/query/pull/8737) [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e) - fix: make mutation variables optional when `undefined extends TVariables`
|
||||
|
||||
- [#11221](https://github.com/TanStack/query/pull/11221) [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93) - Remove experimental render-time prefetching and the `promise` property from query results.
|
||||
|
||||
- [#10584](https://github.com/TanStack/query/pull/10584) [`5199f08`](https://github.com/TanStack/query/commit/5199f0884ca59878bd9e5d352606af13bc5f51fb) - fix(vue-query): preserve `TQueryKey` inference when wrapping `useQuery` in a composable with a generic param
|
||||
|
||||
- Updated dependencies [[`34f7cee`](https://github.com/TanStack/query/commit/34f7ceed09c10e4a3aa2df31a106ddf02ec4e787), [`b4368c4`](https://github.com/TanStack/query/commit/b4368c43792349f6c29d1fb41f7ee1ef3a8bdd2c), [`5bb089d`](https://github.com/TanStack/query/commit/5bb089d153d33933983b7ea80f8d69b51d423698), [`ba4650c`](https://github.com/TanStack/query/commit/ba4650c05e61e33c609e051b948c9f7d31ce70d1), [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b), [`1f631b3`](https://github.com/TanStack/query/commit/1f631b3604aab6567fb3f6c90646a5a304641546), [`01a02bf`](https://github.com/TanStack/query/commit/01a02bfad32f9efbc17796be31d6399f1197a655), [`18c1c1e`](https://github.com/TanStack/query/commit/18c1c1ef94883e781ad36d36f9ea4a043ce4260b), [`5448063`](https://github.com/TanStack/query/commit/5448063f828d2270dccd41ae375e1afde35e91f2), [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e), [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93), [`5981771`](https://github.com/TanStack/query/commit/5981771abec9330b344a2617543eb05ba7c99e24), [`4a9bef6`](https://github.com/TanStack/query/commit/4a9bef6cf19b1cd6b014032d696129f12a848185), [`bef4bc7`](https://github.com/TanStack/query/commit/bef4bc780ce7cca32b7e3dea85f77d92f82a62a2), [`9656dc4`](https://github.com/TanStack/query/commit/9656dc4e5fef5f8c502579b52459f9e8c72e787c), [`326aaf1`](https://github.com/TanStack/query/commit/326aaf1333e5d9cbc46569c53f218d31684162d2), [`3e83601`](https://github.com/TanStack/query/commit/3e836010032c2586e9f1c66a271fa9114a6401f9), [`c6fc17c`](https://github.com/TanStack/query/commit/c6fc17cfad6e452261c585fabfb8c210e60531ed)]:
|
||||
- @tanstack/query-core@5.102.0
|
||||
|
||||
## 5.101.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.4
|
||||
|
||||
## 5.101.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
|
||||
- @tanstack/query-core@5.101.3
|
||||
|
||||
## 5.101.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.2
|
||||
|
||||
## 5.101.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]:
|
||||
- @tanstack/query-core@5.101.1
|
||||
|
||||
## 5.101.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10826](https://github.com/TanStack/query/pull/10826) [`1348095`](https://github.com/TanStack/query/commit/134809544c27cc033ef696d79e3fe4bf75d1e6a8) - Replace deprecated isServer with environmentManager.isServer()
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.0
|
||||
|
||||
## 5.100.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.14
|
||||
|
||||
## 5.100.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(query-core): drop the custom `NoInfer<T>` re-export and rely on TypeScript's built-in `NoInfer` (TS ≥ 5.4) so `NoInfer<X[K]>` stays assignable to `X[K]` in generic contexts (fixes [#9937](https://github.com/TanStack/query/issues/9937)) ([#10593](https://github.com/TanStack/query/pull/10593))
|
||||
|
||||
- Updated dependencies [[`d423168`](https://github.com/TanStack/query/commit/d423168f6261a5cb3d353e53b27c8150cc271151)]:
|
||||
- @tanstack/query-core@5.100.13
|
||||
|
||||
## 5.100.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.12
|
||||
|
||||
## 5.100.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(vue-query): allow reactive and getter values as `queryKey` property in `queryOptions` ([#10530](https://github.com/TanStack/query/pull/10530))
|
||||
|
||||
This fixes a regression introduced in #10452 where `queryOptions` only accepted plain arrays for the `queryKey` property, but not `computed` refs, `Ref` values, or getter functions. The related fix in #10465 only covered the `enabled` property.
|
||||
|
||||
Now the `queryKey` property in `queryOptions` accepts the same reactive forms as `enabled`:
|
||||
- Plain `QueryKey` arrays
|
||||
- `Ref<QueryKey>`
|
||||
- `ComputedRef<QueryKey>`
|
||||
- `() => QueryKey` (getter)
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.11
|
||||
|
||||
## 5.100.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.10
|
||||
|
||||
## 5.100.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fcee7bd`](https://github.com/TanStack/query/commit/fcee7bdc429385ae8ffa224fa8a7a9ec7b8ee380)]:
|
||||
- @tanstack/query-core@5.100.9
|
||||
|
||||
## 5.100.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.8
|
||||
|
||||
## 5.100.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.7
|
||||
|
||||
## 5.100.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.6
|
||||
|
||||
## 5.100.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a53ef97`](https://github.com/TanStack/query/commit/a53ef97f87decb8ea2431710f5199431d3c94c8d)]:
|
||||
- @tanstack/query-core@5.100.5
|
||||
|
||||
## 5.100.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.4
|
||||
|
||||
## 5.100.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f85d825`](https://github.com/TanStack/query/commit/f85d825e02efbbff02e2081528ed28f5e5382f7a)]:
|
||||
- @tanstack/query-core@5.100.3
|
||||
|
||||
## 5.100.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ea4497e`](https://github.com/TanStack/query/commit/ea4497e8aa00d8c1c3a36fb1e17563a889d6ab31), [`d6a7bf3`](https://github.com/TanStack/query/commit/d6a7bf3e3e024c1a77d0536813238cc8007a5fa7), [`645d5d1`](https://github.com/TanStack/query/commit/645d5d130f5e8017cb1bf1a37987f7b980aed705)]:
|
||||
- @tanstack/query-core@5.100.2
|
||||
|
||||
## 5.100.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1bb0d23`](https://github.com/TanStack/query/commit/1bb0d234280fd4ae1725c439088426a20593a8df)]:
|
||||
- @tanstack/query-core@5.100.1
|
||||
|
||||
## 5.100.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6540a41`](https://github.com/TanStack/query/commit/6540a4126b1c087d86d64525e78f32d9920dcd31)]:
|
||||
- @tanstack/query-core@5.100.0
|
||||
|
||||
## 5.99.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(vue-query): allow computed ref and other reactive values as `enabled` property in queryOptions ([#10465](https://github.com/TanStack/query/pull/10465))
|
||||
|
||||
This fixes a regression introduced in #10452 where `queryOptions` only accepted getter functions for the `enabled` property, but not `computed` refs or other reactive values.
|
||||
|
||||
Now the `enabled` property in `queryOptions` correctly accepts:
|
||||
- `boolean` values
|
||||
- `Ref<boolean>`
|
||||
- `ComputedRef<boolean>`
|
||||
- `() => boolean` getter functions
|
||||
- `(query) => boolean` query predicate functions
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.2
|
||||
|
||||
## 5.99.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.1
|
||||
|
||||
## 5.99.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat(vue-query): add 'mutationOptions' ([#10381](https://github.com/TanStack/query/pull/10381))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.0
|
||||
|
||||
## 5.98.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add usePrefetchQuery and usePrefetchInfiniteQuery to vue-query. ([#10372](https://github.com/TanStack/query/pull/10372))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(vue-query): fix type of queryOptions to allow plain properies or getters ([#10452](https://github.com/TanStack/query/pull/10452))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.98.0
|
||||
|
||||
## 5.97.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`2bfb12c`](https://github.com/TanStack/query/commit/2bfb12cc44f1d8495106136e4ddacb817135f8f9)]:
|
||||
- @tanstack/query-core@5.97.0
|
||||
|
||||
## 5.96.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.2
|
||||
|
||||
## 5.96.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.1
|
||||
|
||||
## 5.96.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.0
|
||||
|
||||
## 5.95.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`cd5a35b`](https://github.com/TanStack/query/commit/cd5a35b328837781aa4f9305bb2bd7877ca934e9)]:
|
||||
- @tanstack/query-core@5.95.2
|
||||
|
||||
## 5.95.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f1775c`](https://github.com/TanStack/query/commit/1f1775ca92f2b6c035682947ff3b3424804ff31a)]:
|
||||
- @tanstack/query-core@5.95.1
|
||||
|
||||
## 5.95.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.95.0
|
||||
|
||||
## 5.94.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(\*): resolve issue about excluded build directory ([#10312](https://github.com/TanStack/query/pull/10312))
|
||||
|
||||
- Updated dependencies [[`4b6536d`](https://github.com/TanStack/query/commit/4b6536dfce99036f4e37f52943c6fed3ad0e0a18)]:
|
||||
- @tanstack/query-core@5.94.5
|
||||
|
||||
## 5.94.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: fixed version ([#10064](https://github.com/TanStack/query/pull/10064))
|
||||
|
||||
- Updated dependencies [[`4c75210`](https://github.com/TanStack/query/commit/4c75210ce8235fe3d39b67e1029eff11278927cc)]:
|
||||
- @tanstack/query-core@5.94.4
|
||||
|
||||
## 5.92.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(streamedQuery): maintain error state on reset refetch with initialData defined ([#10287](https://github.com/TanStack/query/pull/10287))
|
||||
|
||||
- Updated dependencies [[`248975e`](https://github.com/TanStack/query/commit/248975e896f585f6eaa505c796e73fcf7bfd1eec)]:
|
||||
- @tanstack/query-core@5.91.2
|
||||
|
||||
## 5.92.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]:
|
||||
- @tanstack/query-core@5.91.1
|
||||
|
||||
## 5.92.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(vue-query/useMutation): add missing '\_defaulted' omit in 'UseMutationOptionsBase' ([#10215](https://github.com/TanStack/query/pull/10215))
|
||||
|
||||
- Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]:
|
||||
- @tanstack/query-core@5.91.0
|
||||
|
||||
## 5.92.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]:
|
||||
- @tanstack/query-core@5.90.20
|
||||
|
||||
## 5.92.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]:
|
||||
- @tanstack/query-core@5.90.19
|
||||
|
||||
## 5.92.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]:
|
||||
- @tanstack/query-core@5.90.18
|
||||
|
||||
## 5.92.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]:
|
||||
- @tanstack/query-core@5.90.17
|
||||
|
||||
## 5.92.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]:
|
||||
- @tanstack/query-core@5.90.16
|
||||
|
||||
## 5.92.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]:
|
||||
- @tanstack/query-core@5.90.15
|
||||
|
||||
## 5.92.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]:
|
||||
- @tanstack/query-core@5.90.14
|
||||
|
||||
## 5.92.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]:
|
||||
- @tanstack/query-core@5.90.13
|
||||
|
||||
## 5.92.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]:
|
||||
- @tanstack/query-core@5.90.12
|
||||
|
||||
## 5.92.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat(vue-query): allow options getters in additional composables ([#9914](https://github.com/TanStack/query/pull/9914))
|
||||
|
||||
## 5.91.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]:
|
||||
- @tanstack/query-core@5.90.11
|
||||
|
||||
## 5.91.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Include TPageParam in enabled of InfiniteQueryObserverOptions ([#9898](https://github.com/TanStack/query/pull/9898))
|
||||
|
||||
## 5.91.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8e2e174`](https://github.com/TanStack/query/commit/8e2e174e9fd2e7b94cd232041e49c9d014d74e26), [`eb559a6`](https://github.com/TanStack/query/commit/eb559a66dc0d77dd46435f624fa64fc068bef9ae)]:
|
||||
- @tanstack/query-core@5.90.10
|
||||
|
||||
## 5.91.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]:
|
||||
- @tanstack/query-core@5.90.9
|
||||
|
||||
## 5.91.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat(vue-query): support useQuery options getter ([#9866](https://github.com/TanStack/query/pull/9866))
|
||||
|
||||
## 5.90.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]:
|
||||
- @tanstack/query-core@5.90.8
|
||||
|
||||
## 5.90.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]:
|
||||
- @tanstack/query-core@5.90.7
|
||||
|
||||
## 5.90.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]:
|
||||
- @tanstack/query-core@5.90.6
|
||||
|
||||
## 5.90.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]:
|
||||
- @tanstack/query-core@5.90.5
|
||||
|
||||
## 5.90.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]:
|
||||
- @tanstack/query-core@5.90.4
|
||||
|
||||
## 5.90.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]:
|
||||
- @tanstack/query-core@5.90.3
|
||||
118
packages/vue-query/README.md
Normal file
118
packages/vue-query/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://tanstack.com/api/readme/query.png?framework=vue&theme=dark"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://tanstack.com/api/readme/query.png?framework=vue"
|
||||
/>
|
||||
<img
|
||||
src="https://tanstack.com/api/readme/query.png?framework=vue"
|
||||
alt="TanStack Vue Query"
|
||||
width="900"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
[](https://github.com/TanStack/query/tree/main/packages/vue-query)
|
||||
|
||||
[](https://www.npmjs.com/package/@tanstack/vue-query)
|
||||
[](https://github.com/TanStack/query/blob/main/LICENSE)
|
||||
[](https://bundlephobia.com/package/@tanstack/vue-query)
|
||||
[](https://www.npmjs.com/package/@tanstack/vue-query)
|
||||
|
||||
# Vue Query
|
||||
|
||||
Hooks for fetching, caching and updating asynchronous data in Vue.
|
||||
|
||||
Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi)
|
||||
|
||||
# Documentation
|
||||
|
||||
Visit https://tanstack.com/query/latest/docs/vue/overview
|
||||
|
||||
# Quick Features
|
||||
|
||||
- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
|
||||
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
|
||||
- Parallel + Dependent Queries
|
||||
- Mutations + Reactive Query Refetching
|
||||
- Multi-layer Cache + Automatic Garbage Collection
|
||||
- Paginated + Cursor-based Queries
|
||||
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
|
||||
- Request Cancellation
|
||||
- (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching
|
||||
- (experimental) SSR support
|
||||
- Dedicated Devtools
|
||||
- [](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported)
|
||||
|
||||
# Quick Start
|
||||
|
||||
1. Install `vue-query`
|
||||
|
||||
```bash
|
||||
$ npm i @tanstack/vue-query
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
$ pnpm add @tanstack/vue-query
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
$ yarn add @tanstack/vue-query
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
$ bun add @tanstack/vue-query
|
||||
```
|
||||
|
||||
> If you are using Vue 2.6, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api)
|
||||
|
||||
2. Initialize **Vue Query** via **VueQueryPlugin**
|
||||
|
||||
```tsx
|
||||
import { createApp } from 'vue'
|
||||
import { VueQueryPlugin } from '@tanstack/vue-query'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).use(VueQueryPlugin).mount('#app')
|
||||
```
|
||||
|
||||
3. Use query
|
||||
|
||||
```tsx
|
||||
import { defineComponent } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MyComponent',
|
||||
setup() {
|
||||
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
|
||||
|
||||
return {
|
||||
query,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
4. If you need to update options on your query dynamically, make sure to pass them as reactive variables
|
||||
|
||||
```tsx
|
||||
const id = ref(1)
|
||||
const enabled = ref(false)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['todos', id],
|
||||
queryFn: () => getTodos(id),
|
||||
enabled,
|
||||
})
|
||||
```
|
||||
6
packages/vue-query/eslint.config.js
Normal file
6
packages/vue-query/eslint.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import rootConfig from './root.eslint.config.js'
|
||||
|
||||
export default [...rootConfig, ...pluginVue.configs['flat/base']]
|
||||
BIN
packages/vue-query/media/vue-query.png
Normal file
BIN
packages/vue-query/media/vue-query.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
4
packages/vue-query/media/vue-query.svg
Normal file
4
packages/vue-query/media/vue-query.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69">
|
||||
<path fill="#ffd94c" stroke="#002c4b" stroke-width="32.63" d="M156.77 42.75L126.55 95.1 96.32 42.75h-48.3l78.53 136 78.53-136z"/>
|
||||
<path fill="none" stroke="#ff4154" stroke-width="12.43" d="M156.77 42.75L126.55 95.1 96.32 42.75h-48.3l78.53 136 78.53-136z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 337 B |
84
packages/vue-query/package.json
Normal file
84
packages/vue-query/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@tanstack/vue-query",
|
||||
"version": "5.102.8",
|
||||
"description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue",
|
||||
"author": "Damian Osipiuk",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/vue-query"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "premove ./build ./coverage ./dist-ts ./.cache/test-types",
|
||||
"compile": "node ../../node_modules/typescript/lib/tsc.js --project tsconfig.json",
|
||||
"test:eslint": "eslint --concurrency=auto ./src",
|
||||
"test:types": "vue-demi-switch 3 && pnpm run \"/^test:types:ts[0-9]{2}$/\"",
|
||||
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts56 --tsBuildInfoFile .cache/test-types/ts56.tsbuildinfo",
|
||||
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts57 --tsBuildInfoFile .cache/test-types/ts57.tsbuildinfo",
|
||||
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts58 --tsBuildInfoFile .cache/test-types/ts58.tsbuildinfo",
|
||||
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts59 --tsBuildInfoFile .cache/test-types/ts59.tsbuildinfo",
|
||||
"test:types:tscurrent": "tsc --project tsconfig.json --outDir .cache/test-types/tscurrent --tsBuildInfoFile .cache/test-types/tscurrent.tsbuildinfo",
|
||||
"test:types:ts70": "node ../../node_modules/typescript70/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts70 --tsBuildInfoFile .cache/test-types/ts70.tsbuildinfo",
|
||||
"test:lib": "pnpm run test:lib:2 && pnpm run test:lib:2.7 && pnpm run test:lib:3",
|
||||
"test:lib:2": "vue-demi-switch 2 vue2 && vitest",
|
||||
"test:lib:2.7": "vue-demi-switch 2.7 vue2.7 && vitest",
|
||||
"test:lib:3": "vue-demi-switch 3 && vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "tsdown --tsconfig tsconfig.prod.json"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "build/legacy/index.d.ts",
|
||||
"main": "build/legacy/index.cjs",
|
||||
"module": "build/legacy/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"import": {
|
||||
"types": "./build/modern/index.d.ts",
|
||||
"default": "./build/modern/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/index.d.cts",
|
||||
"default": "./build/modern/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/query-core": "workspace:*",
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/query-test-utils": "workspace:*",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vue/composition-api": "1.7.2",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"vue": "^3.4.27",
|
||||
"vue2": "npm:vue@2.6",
|
||||
"vue2.7": "npm:vue@2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.1.2",
|
||||
"vue": "^2.6.0 || ^3.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
1
packages/vue-query/root.eslint.config.js
Symbolic link
1
packages/vue-query/root.eslint.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../eslint.config.js
|
||||
1
packages/vue-query/root.tsdown.config.js
Symbolic link
1
packages/vue-query/root.tsdown.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/getTsdownConfig.js
|
||||
7
packages/vue-query/src/__mocks__/useBaseQuery.ts
Normal file
7
packages/vue-query/src/__mocks__/useBaseQuery.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vi } from 'vitest'
|
||||
import type * as UseBaseQueryModule from '../useBaseQuery'
|
||||
|
||||
const { useBaseQuery: originImpl } =
|
||||
await vi.importActual<typeof UseBaseQueryModule>('../useBaseQuery')
|
||||
|
||||
export const useBaseQuery = vi.fn(originImpl)
|
||||
10
packages/vue-query/src/__mocks__/useQueryClient.ts
Normal file
10
packages/vue-query/src/__mocks__/useQueryClient.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { vi } from 'vitest'
|
||||
import { QueryClient } from '../queryClient'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: Infinity },
|
||||
},
|
||||
})
|
||||
|
||||
export const useQueryClient = vi.fn(() => queryClient)
|
||||
22
packages/vue-query/src/__tests__/filterTypes.test-d.ts
Normal file
22
packages/vue-query/src/__tests__/filterTypes.test-d.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import type {
|
||||
MutationFilters as CoreMutationFilters,
|
||||
QueryFilters as CoreQueryFilters,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MutationFilters, QueryFilters } from '../index'
|
||||
|
||||
describe('filter type exports', () => {
|
||||
it('should re-export core query filters', () => {
|
||||
expectTypeOf<QueryFilters>().toEqualTypeOf<CoreQueryFilters>()
|
||||
expectTypeOf<QueryFilters<readonly ['todos', string]>>().toEqualTypeOf<
|
||||
CoreQueryFilters<readonly ['todos', string]>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should re-export core mutation filters', () => {
|
||||
expectTypeOf<MutationFilters>().toEqualTypeOf<CoreMutationFilters>()
|
||||
expectTypeOf<
|
||||
MutationFilters<string, Error, number, boolean>
|
||||
>().toEqualTypeOf<CoreMutationFilters<string, Error, number, boolean>>()
|
||||
})
|
||||
})
|
||||
165
packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts
Normal file
165
packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { dataTagSymbol } from '@tanstack/query-core'
|
||||
import { reactive } from 'vue-demi'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
// Regression test for exported infiniteQueryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedInfiniteQueryOptions = infiniteQueryOptions({
|
||||
queryKey: ['invalid'],
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
const key = queryKey()
|
||||
assertType(
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('should infer types for callbacks', () => {
|
||||
const key = queryKey()
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
staleTime: 1000,
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should work when passed to useInfiniteQuery', () => {
|
||||
const key = queryKey()
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const { data } = reactive(useInfiniteQuery(options))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
it('should work when passed to infiniteQuery', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery({
|
||||
...options,
|
||||
staleTime: 0,
|
||||
pages: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
it('should work when passed to infiniteQuery with select', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
select: (data) => data.pages,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery({
|
||||
...options,
|
||||
staleTime: 0,
|
||||
pages: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Array<string>>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
|
||||
})
|
||||
it('should tag the queryKey even if no promise is returned', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => 'string',
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
select: (data) => data.pages,
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(tagged)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
it('should properly type when passed to setQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(tagged, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
return prev
|
||||
})
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
getNextPageParam: () => null,
|
||||
initialPageParam: null,
|
||||
} as const
|
||||
|
||||
expect(infiniteQueryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
39
packages/vue-query/src/__tests__/mutationCache.test.ts
Normal file
39
packages/vue-query/src/__tests__/mutationCache.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { MutationCache as MutationCacheOrigin } from '@tanstack/query-core'
|
||||
import { MutationCache } from '../mutationCache'
|
||||
|
||||
describe('MutationCache', () => {
|
||||
beforeAll(() => {
|
||||
vi.spyOn(MutationCacheOrigin.prototype, 'find')
|
||||
vi.spyOn(MutationCacheOrigin.prototype, 'findAll')
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const mutationCache = new MutationCache()
|
||||
|
||||
mutationCache.find({
|
||||
mutationKey: ref(['baz']),
|
||||
})
|
||||
|
||||
expect(MutationCacheOrigin.prototype.find).toHaveBeenCalledWith({
|
||||
mutationKey: ['baz'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const mutationCache = new MutationCache()
|
||||
|
||||
mutationCache.findAll({
|
||||
mutationKey: ref(['baz']),
|
||||
})
|
||||
|
||||
expect(MutationCacheOrigin.prototype.findAll).toHaveBeenCalledWith({
|
||||
mutationKey: ['baz'],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
298
packages/vue-query/src/__tests__/mutationOptions.test-d.ts
Normal file
298
packages/vue-query/src/__tests__/mutationOptions.test-d.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { useIsMutating, useMutationState } from '../useMutationState'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationState,
|
||||
WithRequired,
|
||||
} from '@tanstack/query-core'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type { MutationOptions } from '../types'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
mutationOptions({
|
||||
// @ts-expect-error this is a good error, because onMutates does not exist!
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: ['key'],
|
||||
onMutates: 1000,
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: ['key'],
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for onError callback', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => {
|
||||
throw new Error('fail')
|
||||
},
|
||||
mutationKey: ['key'],
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for variables', () => {
|
||||
mutationOptions<number, DefaultError, { id: string }>({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: ['with-vars'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer result type correctly', () => {
|
||||
mutationOptions<number, DefaultError, void, { name: string }>({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: ['key'],
|
||||
onMutate: () => {
|
||||
return { name: 'onMutateResult' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer context type correctly', () => {
|
||||
mutationOptions<number>({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: ['key'],
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should error if mutationFn return type mismatches TData', () => {
|
||||
assertType(
|
||||
mutationOptions<number>({
|
||||
// @ts-expect-error this is a good error, because return type is string, not number
|
||||
mutationFn: async () => Promise.resolve('wrong return'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow mutationKey to be omitted', () => {
|
||||
return mutationOptions({
|
||||
mutationFn: () => Promise.resolve(123),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer all types when not explicitly provided', () => {
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
mutationKey: ['key'],
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
WithRequired<
|
||||
MutationOptions<number, DefaultError, string, unknown>,
|
||||
'mutationKey'
|
||||
>
|
||||
>()
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
Omit<
|
||||
MutationOptions<number, DefaultError, string, unknown>,
|
||||
'mutationKey'
|
||||
>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work when used with useMutation', () => {
|
||||
const mutation = useMutation(
|
||||
mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
expectTypeOf(mutation.data.value).toEqualTypeOf<string | undefined>()
|
||||
|
||||
// should allow when used with useMutation without mutationKey
|
||||
useMutation(
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with useIsMutating', () => {
|
||||
const isMutating = useIsMutating(
|
||||
mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<Ref<number>>()
|
||||
|
||||
useIsMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with queryClient.isMutating', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const isMutating = queryClient.isMutating(
|
||||
mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<number>()
|
||||
|
||||
queryClient.isMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with useMutationState', () => {
|
||||
const mutationState = useMutationState({
|
||||
filters: mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
})
|
||||
expectTypeOf(mutationState.value).toEqualTypeOf<
|
||||
Array<MutationState<unknown, Error, unknown, unknown>>
|
||||
>()
|
||||
|
||||
useMutationState({
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
filters: mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow getter and infer types correctly', () => {
|
||||
const options = mutationOptions(() => ({
|
||||
mutationKey: ['key'] as const,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}))
|
||||
|
||||
const resolved = options()
|
||||
expectTypeOf(resolved.mutationFn).not.toBeUndefined()
|
||||
expectTypeOf(resolved.mutationKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow getter without mutationKey', () => {
|
||||
const options = mutationOptions(() => ({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}))
|
||||
|
||||
const resolved = options()
|
||||
expectTypeOf(resolved.mutationFn).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should error if mutationFn return type mismatches TData (getter)', () => {
|
||||
assertType(
|
||||
mutationOptions<number>(() => ({
|
||||
// @ts-expect-error this is a good error, because return type is string, not number
|
||||
mutationFn: async () => Promise.resolve('wrong return'),
|
||||
})),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer all types when not explicitly provided (getter)', () => {
|
||||
expectTypeOf(
|
||||
mutationOptions(() => ({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
mutationKey: ['key'],
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})),
|
||||
).toEqualTypeOf<
|
||||
() => WithRequired<
|
||||
MutationOptions<number, DefaultError, string, unknown>,
|
||||
'mutationKey'
|
||||
>
|
||||
>()
|
||||
expectTypeOf(
|
||||
mutationOptions(() => ({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})),
|
||||
).toEqualTypeOf<
|
||||
() => Omit<
|
||||
MutationOptions<number, DefaultError, string, unknown>,
|
||||
'mutationKey'
|
||||
>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work when used with useMutation (getter)', () => {
|
||||
const mutation = useMutation(
|
||||
mutationOptions(() => ({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
})),
|
||||
)
|
||||
expectTypeOf(mutation.data.value).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
})
|
||||
753
packages/vue-query/src/__tests__/mutationOptions.test.ts
Normal file
753
packages/vue-query/src/__tests__/mutationOptions.test.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isReactive, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { useIsMutating, useMutationState } from '../useMutationState'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
useQueryClient().clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the getter received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
|
||||
const getter = () =>
|
||||
({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
}) as const
|
||||
|
||||
expect(mutationOptions(getter)).toBe(getter)
|
||||
})
|
||||
|
||||
it('should return the getter received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
|
||||
const getter = () =>
|
||||
({
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
}) as const
|
||||
|
||||
expect(mutationOptions(getter)).toBe(getter)
|
||||
})
|
||||
|
||||
it('should work when used with useMutation (getter without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate, data } = useMutation(mutationOpts)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(data.value).toEqual('data')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 2, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const isMutating = useIsMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
})
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (getter with mutationKey in mutationOptions)', async () => {
|
||||
const keyRef = ref('isMutatingGetter')
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationKey: [keyRef.value],
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
mutate()
|
||||
|
||||
const isMutating = useIsMutating({
|
||||
mutationKey: [keyRef.value],
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutating.value).toEqual(1)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(isMutating.value).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (getter without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (getter)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 2, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (getter, filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const resolvedOpts1 = mutationOpts1()
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const isMutating = useIsMutating({
|
||||
mutationKey: resolvedOpts1.mutationKey,
|
||||
})
|
||||
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
isMutatingArray.push(isMutating.value)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (getter with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
|
||||
const resolvedOpts = mutationOpts()
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating(resolvedOpts))
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating(resolvedOpts))
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (getter without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (getter)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (getter, filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const queryClient = useQueryClient()
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const resolvedOpts1 = mutationOpts1()
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: resolvedOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: resolvedOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (with mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const states = useMutationState({
|
||||
filters: { mutationKey: mutationOpts.mutationKey, status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const states = useMutationState({
|
||||
filters: { status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState', async () => {
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const states = useMutationState({
|
||||
filters: { status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data1', 'data2'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (filter mutationOpts1.mutationKey)', async () => {
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const states = useMutationState({
|
||||
filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data1'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (getter with mutationKey in mutationOptions)', async () => {
|
||||
const keyRef = ref('useMutationStateGetter')
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationKey: [keyRef.value],
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
mutate('foo')
|
||||
|
||||
const states = useMutationState({
|
||||
filters: { mutationKey: [keyRef.value], status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(states.value).toEqual(['foo'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (getter without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const { mutate } = useMutation(mutationOpts)
|
||||
const states = useMutationState({
|
||||
filters: { status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (getter)', async () => {
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const states = useMutationState({
|
||||
filters: { status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data1', 'data2'])
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (getter, filter mutationOpts1.mutationKey)', async () => {
|
||||
const mutationOpts1 = mutationOptions(() => ({
|
||||
mutationKey: ['mutation'],
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
}))
|
||||
const mutationOpts2 = mutationOptions(() => ({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
}))
|
||||
|
||||
const resolvedOpts1 = mutationOpts1()
|
||||
|
||||
const { mutate: mutate1 } = useMutation(mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(mutationOpts2)
|
||||
const states = useMutationState({
|
||||
filters: { mutationKey: resolvedOpts1.mutationKey, status: 'success' },
|
||||
select: (mutation) => mutation.state.data,
|
||||
})
|
||||
|
||||
expect(states.value).toEqual([])
|
||||
|
||||
mutate1()
|
||||
mutate2()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(states.value).toEqual(['data1'])
|
||||
})
|
||||
|
||||
it('should return data in a shallow ref when shallow is true', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => ({ nested: { count: 0 } })),
|
||||
shallow: true,
|
||||
})
|
||||
|
||||
const { mutate, data } = useMutation(mutationOpts)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(data.value).toEqual({ nested: { count: 0 } })
|
||||
expect(isReactive(data.value?.nested)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return data in a shallow ref when shallow is true (getter)', async () => {
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => ({ nested: { count: 0 } })),
|
||||
shallow: true,
|
||||
}))
|
||||
|
||||
const { mutate, data } = useMutation(mutationOpts)
|
||||
|
||||
mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(data.value).toEqual({ nested: { count: 0 } })
|
||||
expect(isReactive(data.value?.nested)).toBe(false)
|
||||
})
|
||||
|
||||
it('should reactively update mutationKey when ref changes in getter', async () => {
|
||||
const key = queryKey()
|
||||
const keyRef = ref('key01')
|
||||
const fnMock = vi.fn((params: string) => sleep(10).then(() => params))
|
||||
const mutationOpts = mutationOptions(() => ({
|
||||
mutationKey: [...key, keyRef.value],
|
||||
mutationFn: fnMock,
|
||||
}))
|
||||
|
||||
const mutation = useMutation(mutationOpts)
|
||||
|
||||
mutation.mutate('data')
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fnMock).toHaveBeenCalledTimes(1)
|
||||
expect(fnMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'data',
|
||||
expect.objectContaining({ mutationKey: [...key, 'key01'] }),
|
||||
)
|
||||
|
||||
keyRef.value = 'key02'
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
mutation.mutate('data')
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fnMock).toHaveBeenCalledTimes(2)
|
||||
expect(fnMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'data',
|
||||
expect.objectContaining({ mutationKey: [...key, 'key02'] }),
|
||||
)
|
||||
})
|
||||
})
|
||||
47
packages/vue-query/src/__tests__/queryCache.test.ts
Normal file
47
packages/vue-query/src/__tests__/queryCache.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { QueryCache as QueryCacheOrigin } from '@tanstack/query-core'
|
||||
import { QueryCache } from '../queryCache'
|
||||
|
||||
describe('QueryCache', () => {
|
||||
beforeAll(() => {
|
||||
vi.spyOn(QueryCacheOrigin.prototype, 'find')
|
||||
vi.spyOn(QueryCacheOrigin.prototype, 'findAll')
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryCache = new QueryCache()
|
||||
|
||||
queryCache.find({
|
||||
queryKey: ['foo', ref('bar')],
|
||||
})
|
||||
|
||||
expect(QueryCacheOrigin.prototype.find).toHaveBeenCalledWith({
|
||||
queryKey: ['foo', 'bar'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should properly unwrap two parameters', () => {
|
||||
const queryCache = new QueryCache()
|
||||
|
||||
queryCache.findAll({
|
||||
queryKey: ['foo', ref('bar')],
|
||||
})
|
||||
|
||||
expect(QueryCacheOrigin.prototype.findAll).toHaveBeenCalledWith({
|
||||
queryKey: ['foo', 'bar'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should default to empty filters', () => {
|
||||
const queryCache = new QueryCache()
|
||||
|
||||
queryCache.findAll()
|
||||
|
||||
expect(QueryCacheOrigin.prototype.findAll).toHaveBeenCalledWith({})
|
||||
})
|
||||
})
|
||||
})
|
||||
259
packages/vue-query/src/__tests__/queryClient.test-d.ts
Normal file
259
packages/vue-query/src/__tests__/queryClient.test-d.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import type { DataTag, InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('getQueryData', () => {
|
||||
it('should be typed if key is tagged', () => {
|
||||
const key = ['key'] as DataTag<Array<string>, number>
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(key)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer unknown if key is not tagged', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(key)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should infer passed generic if passed', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData<number>(key)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should only allow Arrays to be passed', () => {
|
||||
assertType<Parameters<QueryClient['getQueryData']>>([
|
||||
// @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'QueryKey'
|
||||
{ queryKey: 'key' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('setQueryData', () => {
|
||||
it('updater should be typed if key is tagged', () => {
|
||||
const key = ['key'] as DataTag<Array<string>, number>
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(key, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('value should be typed if key is tagged', () => {
|
||||
const key = ['key'] as DataTag<Array<string>, number>
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(key, '1')
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(key, () => '1')
|
||||
|
||||
const data = queryClient.setQueryData(key, 1)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer unknown for updater if key is not tagged', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(key, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<unknown>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should infer unknown for value if key is not tagged', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(key, 'foo')
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should infer passed generic if passed', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData<string>(key, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<string | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer passed generic for value', () => {
|
||||
const key = ['key'] as const
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData<string>(key, 'foo')
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should preserve updater parameter type inference when used in functions with explicit return types', () => {
|
||||
const key = ['key'] as DataTag<Array<string>, number>
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// Simulate usage inside a function with explicit return type
|
||||
// The outer function returns 'unknown' but this shouldn't affect the updater's type inference
|
||||
;(() =>
|
||||
queryClient.setQueryData(key, (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
return data
|
||||
})) satisfies () => unknown
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchInfiniteQuery', () => {
|
||||
it('should allow passing pages', async () => {
|
||||
const key = queryKey()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const data = await new QueryClient().fetchInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
pages: 5,
|
||||
})
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
|
||||
it('should not allow passing getNextPageParam without pages', () => {
|
||||
const key = queryKey()
|
||||
assertType<Parameters<QueryClient['fetchInfiniteQuery']>>([
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not allow passing pages without getNextPageParam', () => {
|
||||
const key = queryKey()
|
||||
assertType<Parameters<QueryClient['fetchInfiniteQuery']>>([
|
||||
// @ts-expect-error Property 'getNextPageParam' is missing
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
initialPageParam: 1,
|
||||
pages: 5,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('query', () => {
|
||||
it('should return the type of the query fn', () => {
|
||||
const result = new QueryClient().query({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Promise<string>>()
|
||||
})
|
||||
|
||||
it('should return the selected type', () => {
|
||||
const result = new QueryClient().query({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
select: (data) => data.length,
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Promise<number>>()
|
||||
})
|
||||
|
||||
it('should not accept top-level options getters', () => {
|
||||
assertType<Parameters<QueryClient['query']>>([
|
||||
// @ts-expect-error One-shot imperative methods do not resolve top-level getters
|
||||
() => ({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('infiniteQuery', () => {
|
||||
it('should return infinite data', async () => {
|
||||
const data = await new QueryClient().infiniteQuery({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
|
||||
it('should return the selected type', () => {
|
||||
const result = new QueryClient().infiniteQuery({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve({ count: 1 }),
|
||||
getNextPageParam: () => 2,
|
||||
initialPageParam: 1,
|
||||
select: (data) => data.pages.map((page) => page.count),
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Promise<Array<number>>>()
|
||||
})
|
||||
|
||||
it('should not accept top-level options getters', () => {
|
||||
assertType<Parameters<QueryClient['infiniteQuery']>>([
|
||||
// @ts-expect-error One-shot imperative methods do not resolve top-level getters
|
||||
() => ({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('should allow passing pages with getNextPageParam', () => {
|
||||
assertType<Parameters<QueryClient['infiniteQuery']>>([
|
||||
{
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
pages: 5,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not allow passing pages without getNextPageParam', () => {
|
||||
assertType<Parameters<QueryClient['infiniteQuery']>>([
|
||||
// @ts-expect-error Property 'getNextPageParam' is missing
|
||||
{
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
initialPageParam: 1,
|
||||
pages: 5,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should preserve page param inference', () => {
|
||||
new QueryClient().infiniteQuery({
|
||||
queryKey: ['key'],
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
return Promise.resolve(pageParam.toString())
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
602
packages/vue-query/src/__tests__/queryClient.test.ts
Normal file
602
packages/vue-query/src/__tests__/queryClient.test.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref, unref } from 'vue-demi'
|
||||
import { QueryClient as QueryClientOrigin } from '@tanstack/query-core'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
|
||||
vi.mock('@tanstack/query-core', async () => {
|
||||
const actual = await vi.importActual<{
|
||||
QueryClient: typeof QueryClientOrigin
|
||||
}>('@tanstack/query-core')
|
||||
|
||||
// Get the prototype methods dynamically
|
||||
const prototypeMethods = Object.getOwnPropertyNames(
|
||||
actual.QueryClient.prototype,
|
||||
).filter((prop): prop is keyof typeof actual.QueryClient.prototype => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(
|
||||
actual.QueryClient.prototype,
|
||||
prop,
|
||||
)
|
||||
return typeof descriptor?.value === 'function' && prop !== 'constructor'
|
||||
})
|
||||
|
||||
// Spy on all methods in the prototype
|
||||
prototypeMethods.forEach((method) => {
|
||||
vi.spyOn(actual.QueryClient.prototype, method)
|
||||
})
|
||||
|
||||
return actual
|
||||
})
|
||||
|
||||
const queryKeyRef = ['foo', ref('bar')]
|
||||
const queryKeyUnref = ['foo', 'bar']
|
||||
|
||||
const fn = () => 'mock'
|
||||
|
||||
describe('QueryCache', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('isFetching', () => {
|
||||
it('should properly unwrap 1 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.isFetching({
|
||||
queryKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.isFetching).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMutating', () => {
|
||||
it('should properly unwrap 1 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.isMutating({
|
||||
mutationKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.isMutating).toHaveBeenCalledWith({
|
||||
mutationKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getQueryData', () => {
|
||||
it('should properly unwrap 1 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getQueryData(queryKeyRef)
|
||||
|
||||
expect(QueryClientOrigin.prototype.getQueryData).toHaveBeenCalledWith(
|
||||
queryKeyUnref,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureQueryData', () => {
|
||||
it('should properly unwrap parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: queryKeyRef,
|
||||
queryFn: fn,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.ensureQueryData).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
queryFn: fn,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getQueriesData', () => {
|
||||
it('should properly unwrap queryKey param', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getQueriesData({ queryKey: queryKeyRef })
|
||||
|
||||
expect(QueryClientOrigin.prototype.getQueriesData).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly unwrap filters param', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getQueriesData({ queryKey: queryKeyRef })
|
||||
|
||||
expect(QueryClientOrigin.prototype.getQueriesData).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setQueryData', () => {
|
||||
it('should properly unwrap 3 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setQueryData(queryKeyRef, fn, {
|
||||
updatedAt: ref(3),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.setQueryData).toHaveBeenCalledWith(
|
||||
queryKeyUnref,
|
||||
fn,
|
||||
{ updatedAt: 3 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setQueriesData', () => {
|
||||
it('should properly unwrap params with queryKey', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, {
|
||||
updatedAt: ref(3),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.setQueriesData).toHaveBeenCalledWith(
|
||||
{ queryKey: queryKeyUnref },
|
||||
fn,
|
||||
{ updatedAt: 3 },
|
||||
)
|
||||
})
|
||||
|
||||
it('should properly unwrap params with filters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, {
|
||||
updatedAt: ref(3),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.setQueriesData).toHaveBeenCalledWith(
|
||||
{ queryKey: queryKeyUnref },
|
||||
fn,
|
||||
{ updatedAt: 3 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getQueryState', () => {
|
||||
it('should properly unwrap 1 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getQueryState(queryKeyRef)
|
||||
|
||||
expect(QueryClientOrigin.prototype.getQueryState).toHaveBeenCalledWith(
|
||||
queryKeyUnref,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeQueries', () => {
|
||||
it('should properly unwrap 1 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.removeQueries({
|
||||
queryKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.removeQueries).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetQueries', () => {
|
||||
it('should properly unwrap 2 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.resetQueries(
|
||||
{
|
||||
queryKey: queryKeyRef,
|
||||
},
|
||||
{ cancelRefetch: ref(false) },
|
||||
)
|
||||
|
||||
expect(QueryClientOrigin.prototype.resetQueries).toHaveBeenCalledWith(
|
||||
{
|
||||
queryKey: queryKeyUnref,
|
||||
},
|
||||
{ cancelRefetch: false },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelQueries', () => {
|
||||
it('should properly unwrap 2 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.cancelQueries(
|
||||
{
|
||||
queryKey: queryKeyRef,
|
||||
},
|
||||
{ revert: ref(false) },
|
||||
)
|
||||
|
||||
expect(QueryClientOrigin.prototype.cancelQueries).toHaveBeenCalledWith(
|
||||
{
|
||||
queryKey: queryKeyUnref,
|
||||
},
|
||||
{ revert: false },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalidateQueries', () => {
|
||||
it('should properly unwrap 2 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: queryKeyRef,
|
||||
},
|
||||
{ cancelRefetch: ref(false) },
|
||||
)
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.invalidateQueries,
|
||||
).toHaveBeenCalledWith(
|
||||
{
|
||||
queryKey: queryKeyUnref,
|
||||
refetchType: 'none',
|
||||
},
|
||||
{ cancelRefetch: false },
|
||||
)
|
||||
})
|
||||
|
||||
// #7694
|
||||
it('should call invalidateQueries immediately and refetchQueries after sleep', async () => {
|
||||
const invalidateQueries = vi.spyOn(
|
||||
QueryClientOrigin.prototype,
|
||||
'invalidateQueries',
|
||||
)
|
||||
const refetchQueries = vi.spyOn(
|
||||
QueryClientOrigin.prototype,
|
||||
'refetchQueries',
|
||||
)
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(invalidateQueries).toHaveBeenCalled()
|
||||
expect(refetchQueries).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(refetchQueries).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call invalidateQueries immediately and not call refetchQueries', async () => {
|
||||
const invalidateQueries = vi.spyOn(
|
||||
QueryClientOrigin.prototype,
|
||||
'invalidateQueries',
|
||||
)
|
||||
const refetchQueries = vi.spyOn(
|
||||
QueryClientOrigin.prototype,
|
||||
'refetchQueries',
|
||||
)
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeyRef,
|
||||
refetchType: 'none',
|
||||
})
|
||||
|
||||
expect(invalidateQueries).toHaveBeenCalled()
|
||||
expect(refetchQueries).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(refetchQueries).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('refetchQueries', () => {
|
||||
it('should properly unwrap 2 parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.refetchQueries(
|
||||
{
|
||||
queryKey: queryKeyRef,
|
||||
},
|
||||
{ cancelRefetch: ref(false) },
|
||||
)
|
||||
|
||||
expect(QueryClientOrigin.prototype.refetchQueries).toHaveBeenCalledWith(
|
||||
{
|
||||
queryKey: queryKeyUnref,
|
||||
},
|
||||
{ cancelRefetch: false },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchQuery', () => {
|
||||
it('should properly unwrap parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchQuery({
|
||||
queryKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.fetchQuery).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query', () => {
|
||||
it('should properly unwrap queryKey', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.query({
|
||||
queryKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly unwrap staleTime, and select', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const staleTime = () => 1000
|
||||
const select = (data: string) => data.length
|
||||
|
||||
queryClient.query({
|
||||
queryKey: queryKeyRef,
|
||||
staleTime: ref(staleTime),
|
||||
select: ref(select),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
staleTime,
|
||||
select,
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept explicitly resolved getter options and unwrap queryKey', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const options = queryOptions(() => ({
|
||||
queryKey: queryKeyRef,
|
||||
}))
|
||||
|
||||
queryClient.query(options())
|
||||
|
||||
expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('prefetchQuery', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.prefetchQuery({ queryKey: queryKeyRef, queryFn: fn })
|
||||
|
||||
expect(QueryClientOrigin.prototype.prefetchQuery).toHaveBeenCalledWith({
|
||||
queryKey: queryKeyUnref,
|
||||
queryFn: fn,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchInfiniteQuery', () => {
|
||||
it('should properly unwrap parameter', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchInfiniteQuery({
|
||||
queryKey: queryKeyRef,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.fetchInfiniteQuery,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialPageParam: 0,
|
||||
queryKey: queryKeyUnref,
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('should properly unwrap parameter using infiniteQueryOptions with unref', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: queryKeyUnref,
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: () => 12,
|
||||
})
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchInfiniteQuery(options)
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.fetchInfiniteQuery,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialPageParam: 0,
|
||||
queryKey: queryKeyUnref,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('infiniteQuery', () => {
|
||||
it('should properly unwrap queryKey, initialPageParam, pages, and select', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const getNextPageParam = () => 1
|
||||
const select = (data: { pages: Array<string> }) => data.pages.length
|
||||
|
||||
queryClient.infiniteQuery({
|
||||
queryKey: queryKeyRef,
|
||||
initialPageParam: ref(0),
|
||||
pages: ref(2),
|
||||
getNextPageParam: ref(getNextPageParam),
|
||||
select: ref(select),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.infiniteQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
queryKey: queryKeyUnref,
|
||||
initialPageParam: 0,
|
||||
pages: 2,
|
||||
getNextPageParam,
|
||||
select,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should properly unwrap getNextPageParam when using infiniteQueryOptions', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const getNextPageParam = () => 12
|
||||
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: queryKeyRef,
|
||||
initialPageParam: ref(0),
|
||||
getNextPageParam: ref(getNextPageParam),
|
||||
})
|
||||
|
||||
queryClient.infiniteQuery({
|
||||
...unref(options),
|
||||
staleTime: 0,
|
||||
pages: 1,
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.infiniteQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryKey: queryKeyUnref,
|
||||
initialPageParam: 0,
|
||||
pages: 1,
|
||||
getNextPageParam,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prefetchInfiniteQuery', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.prefetchInfiniteQuery({
|
||||
queryKey: queryKeyRef,
|
||||
queryFn: fn,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.prefetchInfiniteQuery,
|
||||
).toHaveBeenCalledWith({
|
||||
initialPageParam: 0,
|
||||
queryKey: queryKeyUnref,
|
||||
queryFn: fn,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setDefaultOptions', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setDefaultOptions({
|
||||
queries: {
|
||||
enabled: ref(false),
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.setDefaultOptions,
|
||||
).toHaveBeenCalledWith({
|
||||
queries: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setQueryDefaults', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setQueryDefaults(queryKeyRef, {
|
||||
enabled: ref(false),
|
||||
})
|
||||
|
||||
expect(QueryClientOrigin.prototype.setQueryDefaults).toHaveBeenCalledWith(
|
||||
queryKeyUnref,
|
||||
{
|
||||
enabled: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getQueryDefaults', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getQueryDefaults(queryKeyRef)
|
||||
|
||||
expect(QueryClientOrigin.prototype.getQueryDefaults).toHaveBeenCalledWith(
|
||||
queryKeyUnref,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMutationDefaults', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.setMutationDefaults(queryKeyRef, {
|
||||
mutationKey: queryKeyRef,
|
||||
})
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.setMutationDefaults,
|
||||
).toHaveBeenCalledWith(queryKeyUnref, {
|
||||
mutationKey: queryKeyUnref,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMutationDefaults', () => {
|
||||
it('should properly unwrap parameters', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
queryClient.getMutationDefaults(queryKeyRef)
|
||||
|
||||
expect(
|
||||
QueryClientOrigin.prototype.getMutationDefaults,
|
||||
).toHaveBeenCalledWith(queryKeyUnref)
|
||||
})
|
||||
})
|
||||
})
|
||||
397
packages/vue-query/src/__tests__/queryOptions.test-d.ts
Normal file
397
packages/vue-query/src/__tests__/queryOptions.test-d.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { computed, reactive, ref } from 'vue-demi'
|
||||
import { dataTagSymbol, skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
import { useQuery } from '../useQuery'
|
||||
|
||||
// Regression test for exported queryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedQueryOptions = queryOptions({
|
||||
queryKey: ['invalid'],
|
||||
})
|
||||
|
||||
export const exportedQueryOptionsGetter = queryOptions(() => ({
|
||||
queryKey: ['invalid'],
|
||||
}))
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
const key = queryKey()
|
||||
assertType(
|
||||
queryOptions({
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('should infer types for callbacks', () => {
|
||||
const key = queryKey()
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
staleTime: 1000,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should work when passed to useQuery', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const { data } = reactive(useQuery(options))
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should work when passed to query', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should work when passed to query with select', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey even if no promise is returned', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => 5,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with unknown if there is no queryFn', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<unknown>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(tagged)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type updaterFn when passed to setQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(tagged, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type value when passed to setQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, '5')
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, () => '5')
|
||||
|
||||
const data = queryClient.setQueryData(tagged, 5)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should allow to be passed to QueryClient methods while containing getter', () => {
|
||||
const ref1 = ref(1)
|
||||
const ref2 = ref(2)
|
||||
const options = queryOptions(() => ({
|
||||
queryKey: [...queryKey(), ref1.value, { nested: ref2.value }],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
}))
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// Should not error
|
||||
const data = queryClient.invalidateQueries(options)
|
||||
// Should not error
|
||||
// eslint-disable-next-line no-restricted-syntax -- grandfathered direct test
|
||||
const data2 = queryClient.fetchQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Promise<void>>()
|
||||
expectTypeOf(data2).toEqualTypeOf<Promise<number>>()
|
||||
})
|
||||
|
||||
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
|
||||
const key = queryKey()
|
||||
const { data } = reactive(
|
||||
useQuery(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => ({
|
||||
wow: true,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = reactive(
|
||||
useQuery(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
const { data } = reactive(
|
||||
useQuery(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
const { data, isSuccess } = reactive(
|
||||
useQuery(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if (isSuccess) {
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should not have undefined when initialData is provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = reactive(
|
||||
useQuery(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
initialData: 42,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should allow accessing queryFn and other properties on the returned options object', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve([]),
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryFn).not.toBeUndefined()
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
expectTypeOf(options.staleTime).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow accessing queryFn and other properties on the returned options when used with getter', () => {
|
||||
const options = queryOptions(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve([]),
|
||||
}))
|
||||
|
||||
const resolvedGetter = options()
|
||||
|
||||
expectTypeOf(resolvedGetter.queryFn).not.toBeUndefined()
|
||||
expectTypeOf(resolvedGetter.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow computed ref as enabled property', () => {
|
||||
const enabled = computed(() => true)
|
||||
|
||||
// This was broken in #10452, fixed in #10458
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
enabled,
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow ref as enabled property', () => {
|
||||
const enabled = ref(true)
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
enabled,
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow boolean as enabled property', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow getter function as enabled property', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
enabled: () => true,
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow computed ref as queryKey', () => {
|
||||
const id = ref<string | null>('1')
|
||||
|
||||
// This was broken in #10452, the #10465 fix only covered `enabled`
|
||||
const options = queryOptions({
|
||||
queryKey: computed(() => ['foo', id.value] as const),
|
||||
queryFn: () => Promise.resolve({ id: '1' }),
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow ref as queryKey', () => {
|
||||
const keyRef = ref(['foo', '1'] as const)
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: keyRef,
|
||||
queryFn: () => Promise.resolve({ id: '1' }),
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow getter function as queryKey', () => {
|
||||
const id = ref<string | null>('1')
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: () => ['foo', id.value] as const,
|
||||
queryFn: () => Promise.resolve({ id: '1' }),
|
||||
})
|
||||
|
||||
expectTypeOf(options.queryKey).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow computed queryFn resolving to skipToken', () => {
|
||||
const id = ref<string | null>('1')
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: computed(() => ['foo', id.value]),
|
||||
queryFn: computed(() =>
|
||||
id.value ? () => Promise.resolve({ id: '1' }) : skipToken,
|
||||
),
|
||||
})
|
||||
|
||||
const { data } = reactive(useQuery(options))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ id: string } | undefined>()
|
||||
})
|
||||
|
||||
it('should allow skipToken inside a whole-options getter', () => {
|
||||
const id = ref<string | null>('1')
|
||||
|
||||
const options = queryOptions(() => {
|
||||
const current = id.value
|
||||
return {
|
||||
queryKey: ['foo', current],
|
||||
queryFn: current ? () => Promise.resolve({ id: current }) : skipToken,
|
||||
}
|
||||
})
|
||||
|
||||
const { data } = reactive(useQuery(options))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ id: string } | undefined>()
|
||||
})
|
||||
})
|
||||
16
packages/vue-query/src/__tests__/queryOptions.test.ts
Normal file
16
packages/vue-query/src/__tests__/queryOptions.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
import type { QueryOptions } from '../queryOptions'
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object: QueryOptions = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
} as const
|
||||
|
||||
const options = queryOptions(object)
|
||||
expect(options).toBe(object)
|
||||
})
|
||||
})
|
||||
154
packages/vue-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
154
packages/vue-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { computed, reactive, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('Discriminated union return type', () => {
|
||||
it('data should be possibly undefined by default', () => {
|
||||
const key = queryKey()
|
||||
const query = reactive(
|
||||
useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(query.data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('data should be defined when query is success', () => {
|
||||
const key = queryKey()
|
||||
const query = reactive(
|
||||
useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isSuccess) {
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(query.data).toEqualTypeOf<InfiniteData<string, unknown>>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be null when query is success', () => {
|
||||
const key = queryKey()
|
||||
const query = reactive(
|
||||
useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.error).toEqualTypeOf<null>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should be undefined when query is pending', () => {
|
||||
const key = queryKey()
|
||||
const query = reactive(
|
||||
useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isPending) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<undefined>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be defined when query is error', () => {
|
||||
const key = queryKey()
|
||||
const query = reactive(
|
||||
useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isError) {
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should accept computed options', () => {
|
||||
const key = queryKey()
|
||||
const options = computed(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}))
|
||||
const query = reactive(useInfiniteQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<InfiniteData<string, unknown>>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should accept computed options using infiniteQueryOptions', () => {
|
||||
const key = queryKey()
|
||||
const options = computed(() =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
const query = reactive(useInfiniteQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<InfiniteData<string, unknown>>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should accept plain options using infiniteQueryOptions', () => {
|
||||
const key = queryKey()
|
||||
const options = () =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
const query = reactive(useInfiniteQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<InfiniteData<string, unknown>>()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryKey reactivity rules', () => {
|
||||
it('should reject a bare reactive getter for the whole queryKey array', () => {
|
||||
const id = ref(1)
|
||||
assertType(
|
||||
useInfiniteQuery({
|
||||
// @ts-expect-error when passed directly to useInfiniteQuery, queryKey cannot be a bare
|
||||
// reactive getter for the whole array (queryOptions() allows this)
|
||||
queryKey: () => ['post', id.value],
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
132
packages/vue-query/src/__tests__/useInfiniteQuery.test.ts
Normal file
132
packages/vue-query/src/__tests__/useInfiniteQuery.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useInfiniteQuery', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should properly execute infinite query', async () => {
|
||||
const key = queryKey()
|
||||
const { data, fetchNextPage, status } = useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(0).then(() => 'data on page ' + pageParam),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: () => 12,
|
||||
})
|
||||
|
||||
expect(data.value).toStrictEqual(undefined)
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(data.value).toStrictEqual({
|
||||
pageParams: [0],
|
||||
pages: ['data on page 0'],
|
||||
})
|
||||
expect(status.value).toStrictEqual('success')
|
||||
|
||||
fetchNextPage()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(data.value).toStrictEqual({
|
||||
pageParams: [0, 12],
|
||||
pages: ['data on page 0', 'data on page 12'],
|
||||
})
|
||||
expect(status.value).toStrictEqual('success')
|
||||
})
|
||||
it('should properly execute infinite query using infiniteQueryOptions', async () => {
|
||||
const key = queryKey()
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(0).then(() => 'data on page ' + pageParam),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: () => 12,
|
||||
})
|
||||
|
||||
const { data, fetchNextPage, status } = useInfiniteQuery(options)
|
||||
|
||||
expect(data.value).toStrictEqual(undefined)
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(data.value).toStrictEqual({
|
||||
pageParams: [0],
|
||||
pages: ['data on page 0'],
|
||||
})
|
||||
expect(status.value).toStrictEqual('success')
|
||||
|
||||
fetchNextPage()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(data.value).toStrictEqual({
|
||||
pageParams: [0, 12],
|
||||
pages: ['data on page 0', 'data on page 12'],
|
||||
})
|
||||
expect(status.value).toStrictEqual('success')
|
||||
})
|
||||
|
||||
it('should react to maxPages changing via a whole-options getter', async () => {
|
||||
const key = queryKey()
|
||||
const maxPages = ref(1)
|
||||
const { data, fetchNextPage } = useInfiniteQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(10).then(() => 'data on page ' + pageParam),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastPage, _allPages, lastPageParam) =>
|
||||
lastPageParam + 1,
|
||||
maxPages: maxPages.value,
|
||||
}))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
fetchNextPage()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(data.value?.pages).toStrictEqual(['data on page 1'])
|
||||
|
||||
maxPages.value = 2
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
fetchNextPage()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(data.value?.pages).toStrictEqual([
|
||||
'data on page 1',
|
||||
'data on page 2',
|
||||
])
|
||||
})
|
||||
|
||||
it('should reflect hasNextPage becoming false once the last page is reached', async () => {
|
||||
const key = queryKey()
|
||||
const { hasNextPage, fetchNextPage, isFetching } = useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(10).then(() => 'data on page ' + pageParam),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastPage, _allPages, lastPageParam) =>
|
||||
lastPageParam < 12 ? lastPageParam + 12 : undefined,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(hasNextPage.value).toBe(true)
|
||||
|
||||
fetchNextPage()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(hasNextPage.value).toBe(false)
|
||||
expect(isFetching.value).toBe(false)
|
||||
})
|
||||
})
|
||||
122
packages/vue-query/src/__tests__/useIsFetching.test.ts
Normal file
122
packages/vue-query/src/__tests__/useIsFetching.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { onScopeDispose, reactive, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useQuery } from '../useQuery'
|
||||
import { useIsFetching } from '../useIsFetching'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useIsFetching', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should properly return isFetching state', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const { isFetching: isFetchingQuery } = useQuery({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
useQuery({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
expect(isFetchingQuery.value).toStrictEqual(true)
|
||||
expect(isFetching.value).toStrictEqual(2)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isFetchingQuery.value).toStrictEqual(false)
|
||||
expect(isFetching.value).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('should stop listening to changes on onScopeDispose', async () => {
|
||||
const key = queryKey()
|
||||
const onScopeDisposeMock = onScopeDispose as MockedFunction<
|
||||
typeof onScopeDispose
|
||||
>
|
||||
onScopeDisposeMock.mockImplementation((fn) => fn())
|
||||
|
||||
const { status } = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
expect(isFetching.value).toStrictEqual(1)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
expect(isFetching.value).toStrictEqual(1)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
expect(isFetching.value).toStrictEqual(1)
|
||||
|
||||
onScopeDisposeMock.mockReset()
|
||||
})
|
||||
|
||||
it('should properly update filters', async () => {
|
||||
const key = queryKey()
|
||||
const filter = reactive({ stale: false, queryKey: key })
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'Some data'),
|
||||
})
|
||||
const isFetching = useIsFetching(filter)
|
||||
|
||||
expect(isFetching.value).toStrictEqual(0)
|
||||
|
||||
filter.stale = true
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isFetching.value).toStrictEqual(1)
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const staleRef = ref(false)
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'Some data'),
|
||||
})
|
||||
const isFetching = useIsFetching(() => ({
|
||||
stale: staleRef.value,
|
||||
queryKey: key,
|
||||
}))
|
||||
|
||||
expect(isFetching.value).toStrictEqual(0)
|
||||
|
||||
staleRef.value = true
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isFetching.value).toStrictEqual(1)
|
||||
})
|
||||
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
useIsFetching()
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
114
packages/vue-query/src/__tests__/useIsMutating.test.ts
Normal file
114
packages/vue-query/src/__tests__/useIsMutating.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { onScopeDispose, reactive, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { useIsMutating } from '../useMutationState'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useIsMutating', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should properly return isMutating state', async () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
const mutation2 = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
mutation.mutateAsync('a')
|
||||
mutation2.mutateAsync('b')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(2)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('should stop listening to changes on onScopeDispose', async () => {
|
||||
const onScopeDisposeMock = onScopeDispose as MockedFunction<
|
||||
typeof onScopeDispose
|
||||
>
|
||||
onScopeDisposeMock.mockImplementation((fn) => fn())
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
const mutation2 = useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
mutation.mutateAsync('a')
|
||||
mutation2.mutateAsync('b')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
onScopeDisposeMock.mockReset()
|
||||
})
|
||||
|
||||
it('should properly update filters', async () => {
|
||||
const key = queryKey()
|
||||
const filterKey = queryKey()
|
||||
const filter = reactive({ mutationKey: filterKey })
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
mutate('foo')
|
||||
|
||||
const isMutating = useIsMutating(filter)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
filter.mutationKey = key
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(1)
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const keyRef = ref('isMutatingGetter2')
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
mutate('foo')
|
||||
|
||||
const isMutating = useIsMutating(() => ({
|
||||
mutationKey: [keyRef.value],
|
||||
}))
|
||||
|
||||
expect(isMutating.value).toStrictEqual(0)
|
||||
|
||||
keyRef.value = key[0]!
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(isMutating.value).toStrictEqual(1)
|
||||
})
|
||||
})
|
||||
156
packages/vue-query/src/__tests__/useMutation.test-d.tsx
Normal file
156
packages/vue-query/src/__tests__/useMutation.test-d.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { reactive } from 'vue-demi'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
import { useMutation } from '../useMutation'
|
||||
import type {
|
||||
MutationFunctionContext,
|
||||
MutationKey,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
describe('Discriminated union return type', () => {
|
||||
it('data should be possibly undefined by default', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('data should be defined when mutation is success', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
if (mutation.isSuccess) {
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be null when mutation is success', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
if (mutation.isSuccess) {
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<null>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should be undefined when mutation is pending', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
if (mutation.isPending) {
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<undefined>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be defined when mutation is error', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
if (mutation.isError) {
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<Error>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should narrow variables', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
}),
|
||||
)
|
||||
|
||||
if (mutation.isIdle) {
|
||||
expectTypeOf(mutation.variables).toEqualTypeOf<undefined>()
|
||||
return
|
||||
}
|
||||
if (mutation.isPending) {
|
||||
expectTypeOf(mutation.variables).toEqualTypeOf<string>()
|
||||
return
|
||||
}
|
||||
if (mutation.isSuccess) {
|
||||
expectTypeOf(mutation.variables).toEqualTypeOf<string>()
|
||||
return
|
||||
}
|
||||
expectTypeOf(mutation.variables).toEqualTypeOf<string>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMutation', () => {
|
||||
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
|
||||
const mutation = reactive(
|
||||
useMutation({
|
||||
mutationFn: (_variables: number | undefined) => sleep(0).then(() => 1),
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
|
||||
useMutation({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
expectTypeOf(context.client).toEqualTypeOf<QueryClient>()
|
||||
return Promise.resolve('data')
|
||||
},
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type context as the last argument for every per-call mutate option', () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
})
|
||||
|
||||
mutation.mutate(undefined, {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type context.mutationKey as MutationKey', () => {
|
||||
useMutation({
|
||||
mutationKey: ['todos', 'add'] as const,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context.mutationKey).toEqualTypeOf<
|
||||
MutationKey | undefined
|
||||
>()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
771
packages/vue-query/src/__tests__/useMutation.test.ts
Normal file
771
packages/vue-query/src/__tests__/useMutation.test.ts
Normal file
@@ -0,0 +1,771 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { reactive, ref } from 'vue-demi'
|
||||
import { noop } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
import type { MutationFunctionContext } from '@tanstack/query-core'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useMutation', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should be in idle state initially', () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params) => sleep(0).then(() => params),
|
||||
})
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: true },
|
||||
isPending: { value: false },
|
||||
isError: { value: false },
|
||||
isSuccess: { value: false },
|
||||
})
|
||||
})
|
||||
|
||||
it('should change state after invoking mutate', () => {
|
||||
const result = 'Mock data'
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutate(result)
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: false },
|
||||
isPending: { value: true },
|
||||
isError: { value: false },
|
||||
isSuccess: { value: false },
|
||||
data: { value: undefined },
|
||||
error: { value: null },
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error when request fails', async () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
mutation.mutate()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: false },
|
||||
isPending: { value: false },
|
||||
isError: { value: true },
|
||||
isSuccess: { value: false },
|
||||
data: { value: undefined },
|
||||
error: { value: Error('Some error') },
|
||||
})
|
||||
})
|
||||
|
||||
it('should return data when request succeeds', async () => {
|
||||
const result = 'Mock data'
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutate(result)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: false },
|
||||
isPending: { value: false },
|
||||
isError: { value: false },
|
||||
isSuccess: { value: true },
|
||||
data: { value: 'Mock data' },
|
||||
error: { value: null },
|
||||
})
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const result = 'Mock data'
|
||||
const keyRef = ref('key01')
|
||||
const fnMock = vi.fn((params: string) => sleep(10).then(() => params))
|
||||
const mutation = useMutation(() => ({
|
||||
mutationKey: [...key, keyRef.value],
|
||||
mutationFn: fnMock,
|
||||
}))
|
||||
|
||||
mutation.mutate(result)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fnMock).toHaveBeenCalledTimes(1)
|
||||
expect(fnMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
result,
|
||||
expect.objectContaining({ mutationKey: [...key, 'key01'] }),
|
||||
)
|
||||
|
||||
keyRef.value = 'key02'
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
mutation.mutate(result)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fnMock).toHaveBeenCalledTimes(2)
|
||||
expect(fnMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
result,
|
||||
expect.objectContaining({ mutationKey: [...key, 'key02'] }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should update reactive options', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queryClient = useQueryClient()
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const options = reactive({
|
||||
mutationKey: key1,
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
const mutation = useMutation(options)
|
||||
|
||||
options.mutationKey = key2
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
mutation.mutate('xyz')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
const mutations = mutationCache.find({ mutationKey: key2 })
|
||||
|
||||
expect(mutations?.options.mutationKey).toEqual(key2)
|
||||
})
|
||||
|
||||
it('should update reactive options deeply', async () => {
|
||||
type MutationKeyTest = {
|
||||
entity: string
|
||||
otherObject: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
const mutationKey = ref<Array<MutationKeyTest>>([
|
||||
{
|
||||
entity: 'test',
|
||||
otherObject: { name: 'objectName' },
|
||||
},
|
||||
])
|
||||
const queryClient = useQueryClient()
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const options = reactive({
|
||||
mutationKey,
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
const mutation = useMutation(options)
|
||||
|
||||
mutationKey.value[0]!.otherObject.name = 'someOtherObjectName'
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
mutation.mutate('xyz')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
const mutations = mutationCache.getAll()
|
||||
const relevantMutation = mutations.find((m) => {
|
||||
return (
|
||||
Array.isArray(m.options.mutationKey) &&
|
||||
!!m.options.mutationKey[0].otherObject
|
||||
)
|
||||
})
|
||||
|
||||
expect(
|
||||
(relevantMutation?.options.mutationKey as Array<MutationKeyTest>)[0]
|
||||
?.otherObject.name === 'someOtherObjectName',
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow for non-options object (mutationFn or mutationKey) passed as arg1 & arg2 to trigger reactive updates', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const mutationKeyRef = ref<Array<string>>(key1)
|
||||
const mutationFn = ref((params: string) => sleep(0).then(() => params))
|
||||
const queryClient = useQueryClient()
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const mutation = useMutation({ mutationKey: mutationKeyRef, mutationFn })
|
||||
|
||||
mutationKeyRef.value = key2
|
||||
let proof = false
|
||||
mutationFn.value = (params: string) => {
|
||||
proof = true
|
||||
return sleep(10).then(() => params)
|
||||
}
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
mutation.mutate('xyz')
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
const mutations = mutationCache.find({ mutationKey: key2 })
|
||||
expect(mutations?.options.mutationKey).toEqual(key2)
|
||||
expect(proof).toEqual(true)
|
||||
})
|
||||
|
||||
it('should reset state after invoking mutation.reset', async () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
mutation.mutate()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
mutation.reset()
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: true },
|
||||
isPending: { value: false },
|
||||
isError: { value: false },
|
||||
isSuccess: { value: false },
|
||||
data: { value: undefined },
|
||||
error: { value: null },
|
||||
})
|
||||
})
|
||||
|
||||
describe('side effects', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call onMutate when passed as an option', async () => {
|
||||
const onMutate = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
onMutate,
|
||||
})
|
||||
|
||||
mutation.mutate('')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onMutate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onError when passed as an option', async () => {
|
||||
const onError = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
onError,
|
||||
})
|
||||
|
||||
mutation.mutate('')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSuccess when passed as an option', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
onSuccess,
|
||||
})
|
||||
|
||||
mutation.mutate('')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSettled when passed as an option', async () => {
|
||||
const onSettled = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
onSettled,
|
||||
})
|
||||
|
||||
mutation.mutate('')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onError when passed as an argument of mutate function', async () => {
|
||||
const onError = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
mutation.mutate(undefined, { onError })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSuccess when passed as an argument of mutate function', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutate('', { onSuccess })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSettled when passed as an argument of mutate function', async () => {
|
||||
const onSettled = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutate('', { onSettled })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSuccess and onSettled when passed as arguments of mutate function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutate('', {
|
||||
onSuccess: () => callbacks.push('mutate.onSuccess'),
|
||||
onSettled: () => callbacks.push('mutate.onSettled'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onSuccess', 'mutate.onSettled'])
|
||||
})
|
||||
|
||||
it('should call onError and onSettled when passed as arguments of mutate function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (_params: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
mutation.mutate('', {
|
||||
onError: () => callbacks.push('mutate.onError'),
|
||||
onSettled: () => callbacks.push('mutate.onSettled'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onError', 'mutate.onSettled'])
|
||||
})
|
||||
|
||||
it('should call onSuccess and onSettled when passed as arguments of mutateAsync function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutateAsync('', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'mutateAsync.onSuccess',
|
||||
'mutateAsync.onSettled',
|
||||
])
|
||||
})
|
||||
|
||||
it('should call onError and onSettled when passed as arguments of mutateAsync function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (_params: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
mutation
|
||||
.mutateAsync('', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
.catch(noop)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'mutateAsync.onError',
|
||||
'mutateAsync.onSettled',
|
||||
])
|
||||
})
|
||||
|
||||
it('should call onSuccess when passed as an argument of mutateAsync function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutateAsync('', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onSuccess'])
|
||||
})
|
||||
|
||||
it('should call onError when passed as an argument of mutateAsync function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (_params: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
mutation
|
||||
.mutateAsync('', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
})
|
||||
.catch(noop)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onError'])
|
||||
})
|
||||
|
||||
it('should call onSettled when passed as an argument of mutateAsync function', async () => {
|
||||
const callbacks: Array<string> = []
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutation.mutateAsync('', {
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onSettled'])
|
||||
})
|
||||
|
||||
it('should fire both onSettled functions', async () => {
|
||||
const onSettled = vi.fn()
|
||||
const onSettledOnFunction = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
onSettled,
|
||||
})
|
||||
|
||||
mutation.mutate('', { onSettled: onSettledOnFunction })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
expect(onSettledOnFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('async', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should resolve properly', async () => {
|
||||
const result = 'Mock data'
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
const promise = mutation.mutateAsync(result)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
await expect(promise).resolves.toBe(result)
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: false },
|
||||
isPending: { value: false },
|
||||
isError: { value: false },
|
||||
isSuccess: { value: true },
|
||||
data: { value: 'Mock data' },
|
||||
error: { value: null },
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw on error', async () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
expect(mutation.mutateAsync()).rejects.toThrow('Some error'),
|
||||
vi.advanceTimersByTimeAsync(10),
|
||||
])
|
||||
|
||||
expect(mutation).toMatchObject({
|
||||
isIdle: { value: false },
|
||||
isPending: { value: false },
|
||||
isError: { value: true },
|
||||
isSuccess: { value: false },
|
||||
data: { value: undefined },
|
||||
error: { value: Error('Some error') },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
useMutation({
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
describe('throwOnError', () => {
|
||||
it('should evaluate throwOnError when mutation is expected to throw', async () => {
|
||||
const err = new Error('Expected mock error. All is well!')
|
||||
const boundaryFn = vi.fn()
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: () => sleep(10).then(() => Promise.reject(err)),
|
||||
throwOnError: boundaryFn,
|
||||
})
|
||||
|
||||
mutate()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(boundaryFn).toHaveBeenCalledTimes(1)
|
||||
expect(boundaryFn).toHaveBeenCalledWith(err)
|
||||
})
|
||||
|
||||
it('should throw from error watcher when throwOnError returns true', async () => {
|
||||
const throwOnErrorFn = vi.fn().mockReturnValue(true)
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
throwOnError: throwOnErrorFn,
|
||||
})
|
||||
|
||||
mutate()
|
||||
|
||||
// Suppress the Unhandled Rejection caused by watcher throw in Vue 3
|
||||
const rejectionHandler = () => {}
|
||||
process.on('unhandledRejection', rejectionHandler)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
process.off('unhandledRejection', rejectionHandler)
|
||||
|
||||
expect(throwOnErrorFn).toHaveBeenCalledTimes(1)
|
||||
expect(throwOnErrorFn).toHaveBeenCalledWith(Error('Some error'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('optimistic updates', () => {
|
||||
it('should update the cache in onMutate and roll back via onMutateResult in onError', async () => {
|
||||
const key = queryKey()
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
onMutate: async (newTodo: string) => {
|
||||
await queryClient.cancelQueries({ queryKey: key })
|
||||
const previousTodos = queryClient.getQueryData<Array<string>>(key)
|
||||
|
||||
queryClient.setQueryData<Array<string>>(key, (old) => [
|
||||
...(old ?? []),
|
||||
newTodo,
|
||||
])
|
||||
|
||||
return { previousTodos }
|
||||
},
|
||||
onError: (_err, _newTodo, onMutateResult) => {
|
||||
queryClient.setQueryData(key, onMutateResult?.previousTodos)
|
||||
},
|
||||
})
|
||||
|
||||
mutation.mutate('Todo 2')
|
||||
|
||||
// onMutate runs synchronously up to its first await, so the optimistic
|
||||
// value is visible immediately, before the mutationFn settles.
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(mutation).toMatchObject({ isError: { value: true } })
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1'])
|
||||
})
|
||||
|
||||
it('should keep the optimistic update in place when the mutation succeeds', async () => {
|
||||
const key = queryKey()
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (newTodo: string) => sleep(10).then(() => newTodo),
|
||||
onMutate: async (newTodo: string) => {
|
||||
await queryClient.cancelQueries({ queryKey: key })
|
||||
const previousTodos = queryClient.getQueryData<Array<string>>(key)
|
||||
|
||||
queryClient.setQueryData<Array<string>>(key, (old) => [
|
||||
...(old ?? []),
|
||||
newTodo,
|
||||
])
|
||||
|
||||
return { previousTodos }
|
||||
},
|
||||
onError: (_err, _newTodo, onMutateResult) => {
|
||||
queryClient.setQueryData(key, onMutateResult?.previousTodos)
|
||||
},
|
||||
})
|
||||
|
||||
mutation.mutate('Todo 2')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(mutation).toMatchObject({ isSuccess: { value: true } })
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('concurrent mutate calls', () => {
|
||||
it('should report each call result independently when some fail', async () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (todo: string) =>
|
||||
todo === 'bad'
|
||||
? sleep(10).then(() => Promise.reject(new Error('Some error')))
|
||||
: sleep(10).then(() => todo),
|
||||
})
|
||||
|
||||
const todos = ['Todo 1', 'bad', 'Todo 3']
|
||||
|
||||
const settledPromise = Promise.allSettled(
|
||||
todos.map((todo) => mutation.mutateAsync(todo)),
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
const results = await settledPromise
|
||||
|
||||
expect(results).toEqual([
|
||||
{ status: 'fulfilled', value: 'Todo 1' },
|
||||
{ status: 'rejected', reason: Error('Some error') },
|
||||
{ status: 'fulfilled', value: 'Todo 3' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should only fire the per-call onSuccess for the last mutate() call', async () => {
|
||||
const onSuccessPerCall = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (todo: string) => sleep(10).then(() => todo),
|
||||
})
|
||||
|
||||
mutation.mutate('Todo 1', { onSuccess: onSuccessPerCall })
|
||||
mutation.mutate('Todo 2', { onSuccess: onSuccessPerCall })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccessPerCall).toHaveBeenCalledTimes(1)
|
||||
const [data, variables, onMutateResult, context] =
|
||||
onSuccessPerCall.mock.calls[0]!
|
||||
expect(data).toBe('Todo 2')
|
||||
expect(variables).toBe('Todo 2')
|
||||
expect(onMutateResult).toBeUndefined()
|
||||
expect(context.client).toBe(useQueryClient())
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text.toUpperCase()),
|
||||
onMutate: (text: string) => ({ startedWith: text }),
|
||||
onSuccess,
|
||||
})
|
||||
|
||||
mutation.mutate('todo')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
const [data, variables, onMutateResult, context] = onSuccess.mock.calls[0]!
|
||||
expect(data).toBe('TODO')
|
||||
expect(variables).toBe('todo')
|
||||
expect(onMutateResult).toEqual({ startedWith: 'todo' })
|
||||
expect(context.client).toBe(useQueryClient())
|
||||
expect(context.meta).toBeUndefined()
|
||||
expect(context.mutationKey).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should give mutationFn the same QueryClient instance via context', async () => {
|
||||
const key = queryKey()
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.setQueryData(key, 'tag-from-this-client')
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (_text: string, context: MutationFunctionContext) =>
|
||||
sleep(10).then(() => context.client.getQueryData(key)),
|
||||
})
|
||||
|
||||
mutation.mutate('todo')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(mutation.data.value).toBe('tag-from-this-client')
|
||||
})
|
||||
|
||||
it('should include mutationKey in the context passed to hook-level callbacks', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['todos', 'add'],
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
onSuccess,
|
||||
})
|
||||
|
||||
mutation.mutate('todo')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(onSuccess.mock.calls[0]?.[3].mutationKey).toEqual(['todos', 'add'])
|
||||
})
|
||||
|
||||
it('should let onSuccess invalidate queries via context.client without a useQueryClient() closure', async () => {
|
||||
const key = queryKey()
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.setQueryData(key, 'data')
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => sleep(10).then(() => 'mutated'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
context.client.invalidateQueries({ queryKey: key })
|
||||
},
|
||||
})
|
||||
|
||||
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(false)
|
||||
|
||||
mutation.mutate()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true)
|
||||
})
|
||||
})
|
||||
81
packages/vue-query/src/__tests__/useMutationState.test.ts
Normal file
81
packages/vue-query/src/__tests__/useMutationState.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { useMutationState } from '../useMutationState'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useMutationState', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return variables after calling mutate 1', () => {
|
||||
const key = queryKey()
|
||||
const variables = 'foo123'
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
|
||||
mutate(variables)
|
||||
|
||||
const mutationState = useMutationState({
|
||||
filters: { mutationKey: key, status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
})
|
||||
|
||||
expect(mutationState.value).toEqual([variables])
|
||||
})
|
||||
|
||||
it('should return variables after calling mutate 2', () => {
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.clear()
|
||||
const key = queryKey()
|
||||
const variables = 'bar234'
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (params: string) => sleep(0).then(() => params),
|
||||
})
|
||||
|
||||
mutate(variables)
|
||||
|
||||
const mutationState = useMutationState()
|
||||
|
||||
expect(mutationState.value[0]?.variables).toEqual(variables)
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const keyRef = ref('useMutationStateGetter2')
|
||||
const variables = 'foo123'
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: key,
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
})
|
||||
|
||||
mutate(variables)
|
||||
|
||||
const mutationState = useMutationState(() => ({
|
||||
filters: { mutationKey: [keyRef.value], status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
}))
|
||||
|
||||
expect(mutationState.value).toEqual([])
|
||||
|
||||
keyRef.value = key[0]!
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(mutationState.value).toEqual([variables])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { usePrefetchInfiniteQuery } from '..'
|
||||
|
||||
describe('usePrefetchInfiniteQuery', () => {
|
||||
it('should return nothing', () => {
|
||||
const result = usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<void>()
|
||||
})
|
||||
|
||||
it('should require initialPageParam and getNextPageParam', () => {
|
||||
assertType(
|
||||
// @ts-expect-error TS2345
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not allow refetchInterval, enabled or throwOnError options', () => {
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
// @ts-expect-error TS2353
|
||||
refetchInterval: 1000,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
// @ts-expect-error TS2353
|
||||
enabled: true,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
// @ts-expect-error TS2353
|
||||
throwOnError: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should accept refs in infinite query options', () => {
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: [...queryKey(), ref('id')],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
initialPageParam: ref(1),
|
||||
getNextPageParam: () => 1,
|
||||
staleTime: ref(1000),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not allow skipToken in queryFn', () => {
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
// @ts-expect-error
|
||||
queryFn: skipToken,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => 1,
|
||||
// @ts-expect-error
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue-demi'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { usePrefetchInfiniteQuery } from '../usePrefetchInfiniteQuery'
|
||||
|
||||
describe('usePrefetchInfiniteQuery', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should prefetch infinite query if query state does not exist', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery')
|
||||
const queryFn = () =>
|
||||
Promise.resolve({ data: 'prefetched', currentPage: 1 })
|
||||
const getNextPageParam = () => undefined
|
||||
|
||||
const key = queryKey()
|
||||
|
||||
usePrefetchInfiniteQuery(
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(infiniteQuerySpy).toHaveBeenCalledTimes(1)
|
||||
expect(infiniteQuerySpy).toHaveBeenCalledWith({
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not prefetch infinite query if query state exists', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery')
|
||||
const queryFn = () =>
|
||||
Promise.resolve({ data: 'prefetched', currentPage: 1 })
|
||||
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData(key, {
|
||||
pages: [{ data: 'existing', currentPage: 1 }],
|
||||
pageParams: [1],
|
||||
})
|
||||
|
||||
usePrefetchInfiniteQuery(
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(infiniteQuerySpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unwrap refs in infinite query options', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery')
|
||||
const nestedRef = ref('value')
|
||||
const key = queryKey()
|
||||
const queryFn = () =>
|
||||
Promise.resolve({ data: 'prefetched', currentPage: 1 })
|
||||
const getNextPageParam = () => undefined
|
||||
|
||||
usePrefetchInfiniteQuery(
|
||||
{
|
||||
queryKey: [...key, nestedRef],
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(infiniteQuerySpy).toHaveBeenCalledWith({
|
||||
queryKey: [...key, 'value'],
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
})
|
||||
})
|
||||
|
||||
it('should prefetch infinite query again when query key changes reactively', async () => {
|
||||
const queryClient = new QueryClient()
|
||||
const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery')
|
||||
const keyRef = ref('first')
|
||||
const key = queryKey()
|
||||
const queryFn = () =>
|
||||
Promise.resolve({ data: keyRef.value, currentPage: 1 })
|
||||
const getNextPageParam = () => undefined
|
||||
|
||||
usePrefetchInfiniteQuery(
|
||||
() => ({
|
||||
queryKey: [...key, keyRef.value],
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
}),
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(infiniteQuerySpy).toHaveBeenCalledTimes(1)
|
||||
expect(infiniteQuerySpy).toHaveBeenNthCalledWith(1, {
|
||||
queryKey: [...key, 'first'],
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
})
|
||||
|
||||
keyRef.value = 'second'
|
||||
await nextTick()
|
||||
|
||||
expect(infiniteQuerySpy).toHaveBeenCalledTimes(2)
|
||||
expect(infiniteQuerySpy).toHaveBeenNthCalledWith(2, {
|
||||
queryKey: [...key, 'second'],
|
||||
queryFn,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam,
|
||||
})
|
||||
})
|
||||
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
usePrefetchInfiniteQuery(
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () =>
|
||||
Promise.resolve({ data: 'prefetched', currentPage: 1 }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
},
|
||||
new QueryClient(),
|
||||
)
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
73
packages/vue-query/src/__tests__/usePrefetchQuery.test-d.ts
Normal file
73
packages/vue-query/src/__tests__/usePrefetchQuery.test-d.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { ref } from 'vue-demi'
|
||||
import { skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { usePrefetchQuery } from '..'
|
||||
|
||||
describe('usePrefetchQuery', () => {
|
||||
it('should return nothing', () => {
|
||||
const result = usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<void>()
|
||||
})
|
||||
|
||||
it('should not allow refetchInterval, enabled or throwOnError options', () => {
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error TS2353
|
||||
refetchInterval: 1000,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error TS2353
|
||||
enabled: true,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error TS2353
|
||||
throwOnError: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should accept refs in query options', () => {
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: [...queryKey(), ref('id')],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
staleTime: ref(1000),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not allow skipToken in queryFn', () => {
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
// @ts-expect-error
|
||||
queryFn: skipToken,
|
||||
}),
|
||||
)
|
||||
|
||||
assertType(
|
||||
usePrefetchQuery({
|
||||
queryKey: queryKey(),
|
||||
// @ts-expect-error
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
128
packages/vue-query/src/__tests__/usePrefetchQuery.test.ts
Normal file
128
packages/vue-query/src/__tests__/usePrefetchQuery.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue-demi'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { usePrefetchQuery } from '../usePrefetchQuery'
|
||||
|
||||
describe('usePrefetchQuery', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should prefetch query if query state does not exist', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const querySpy = vi.spyOn(queryClient, 'query')
|
||||
const queryFn = () => Promise.resolve('prefetched')
|
||||
const key = queryKey()
|
||||
|
||||
usePrefetchQuery(
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(querySpy).toHaveBeenCalledTimes(1)
|
||||
expect(querySpy).toHaveBeenCalledWith({
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not prefetch query if query state exists', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const querySpy = vi.spyOn(queryClient, 'query')
|
||||
const queryFn = () => Promise.resolve('prefetched')
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData(key, 'existing')
|
||||
|
||||
usePrefetchQuery(
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(querySpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unwrap refs in query options', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const querySpy = vi.spyOn(queryClient, 'query')
|
||||
const nestedRef = ref('value')
|
||||
const key = queryKey()
|
||||
const queryFn = () => Promise.resolve('prefetched')
|
||||
|
||||
usePrefetchQuery(
|
||||
{
|
||||
queryKey: [...key, nestedRef],
|
||||
queryFn,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(querySpy).toHaveBeenCalledWith({
|
||||
queryKey: [...key, 'value'],
|
||||
queryFn,
|
||||
})
|
||||
})
|
||||
|
||||
it('should prefetch again when query key changes reactively', async () => {
|
||||
const queryClient = new QueryClient()
|
||||
const querySpy = vi.spyOn(queryClient, 'query')
|
||||
const keyRef = ref('first')
|
||||
const key = queryKey()
|
||||
const queryFn = () => Promise.resolve(keyRef.value)
|
||||
|
||||
usePrefetchQuery(
|
||||
() => ({
|
||||
queryKey: [...key, keyRef.value],
|
||||
queryFn,
|
||||
}),
|
||||
queryClient,
|
||||
)
|
||||
|
||||
expect(querySpy).toHaveBeenCalledTimes(1)
|
||||
expect(querySpy).toHaveBeenNthCalledWith(1, {
|
||||
queryKey: [...key, 'first'],
|
||||
queryFn,
|
||||
})
|
||||
|
||||
keyRef.value = 'second'
|
||||
await nextTick()
|
||||
|
||||
expect(querySpy).toHaveBeenCalledTimes(2)
|
||||
expect(querySpy).toHaveBeenNthCalledWith(2, {
|
||||
queryKey: [...key, 'second'],
|
||||
queryFn,
|
||||
})
|
||||
})
|
||||
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
usePrefetchQuery(
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('prefetched'),
|
||||
},
|
||||
new QueryClient(),
|
||||
)
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
289
packages/vue-query/src/__tests__/useQueries.test-d.ts
Normal file
289
packages/vue-query/src/__tests__/useQueries.test-d.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { reactive } from 'vue'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { skipToken, useQueries } from '..'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
import type { OmitKeyof, QueryObserverResult } from '..'
|
||||
import type { UseQueryOptions } from '../queryOptions'
|
||||
|
||||
describe('UseQueries config object overload', () => {
|
||||
it('TData should always be defined when initialData is provided as an object', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
const query1 = {
|
||||
queryKey: key1,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: false,
|
||||
},
|
||||
}
|
||||
|
||||
const query2 = queryOptions({
|
||||
queryKey: key2,
|
||||
queryFn: () => 'Query Data',
|
||||
initialData: 'initial data',
|
||||
})
|
||||
|
||||
const query3 = {
|
||||
queryKey: key3,
|
||||
queryFn: () => 'Query Data',
|
||||
}
|
||||
|
||||
const { value: queriesState } = useQueries({
|
||||
queries: [query1, query2, query3],
|
||||
})
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>()
|
||||
expectTypeOf(queriesState[1].data).toEqualTypeOf<string>()
|
||||
expectTypeOf(queriesState[2].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('TData should be defined when passed through queryOptions', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { value: queriesState } = useQueries({ queries: [options] })
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const query1 = queryOptions({
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data) => data > 1,
|
||||
})
|
||||
|
||||
const query2 = {
|
||||
queryKey: key2,
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data: any) => data > 1,
|
||||
}
|
||||
|
||||
const queriesState = reactive(useQueries({ queries: [query1, query2] }))
|
||||
|
||||
expectTypeOf(queriesState.value[0].data).toEqualTypeOf<
|
||||
boolean | undefined
|
||||
>()
|
||||
expectTypeOf(queriesState.value[1].data).toEqualTypeOf<
|
||||
boolean | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
const { value: queriesState } = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<
|
||||
{ wow: boolean } | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('TData should have correct type when conditional skipToken is passed', () => {
|
||||
const key = queryKey()
|
||||
const { value: queriesState } = useQueries({
|
||||
queries: [
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const firstResult = queriesState[0]
|
||||
|
||||
expectTypeOf(firstResult).toEqualTypeOf<
|
||||
QueryObserverResult<number, Error>
|
||||
>()
|
||||
expectTypeOf(firstResult.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
describe('custom hook', () => {
|
||||
it('should allow custom hooks using UseQueryOptions', () => {
|
||||
const useCustomQueries = (
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<string>,
|
||||
'queryKey' | 'queryFn',
|
||||
'safely'
|
||||
>,
|
||||
) =>
|
||||
useQueries({
|
||||
queries: [
|
||||
{
|
||||
...options,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const { value: queriesState } = useCustomQueries()
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
// Fix #7270
|
||||
it('should have proper type inference with different options provided', () => {
|
||||
const numbers = [1, 2, 3]
|
||||
const getQueryKey = (n: number) => [n]
|
||||
const queryFn = (n: number) => () => Promise.resolve(n)
|
||||
const select = (data: number) => data.toString()
|
||||
|
||||
const queries = numbers.map((n) => ({
|
||||
queryKey: [n],
|
||||
queryFn: () => Promise.resolve(n),
|
||||
select: (data: number) => data.toString(),
|
||||
}))
|
||||
|
||||
const queriesWithoutSelect = numbers.map((n) => ({
|
||||
queryKey: getQueryKey(n),
|
||||
queryFn: queryFn(n),
|
||||
}))
|
||||
|
||||
const queriesWithQueryOptions = numbers.map((n) =>
|
||||
queryOptions({
|
||||
queryKey: getQueryKey(n),
|
||||
queryFn: queryFn(n),
|
||||
select,
|
||||
}),
|
||||
)
|
||||
|
||||
const queriesWithQueryOptionsWithoutSelect = numbers.map((n) =>
|
||||
queryOptions({
|
||||
queryKey: getQueryKey(n),
|
||||
queryFn: queryFn(n),
|
||||
}),
|
||||
)
|
||||
|
||||
const query1 = useQueries({ queries: queries })
|
||||
expectTypeOf(query1.value).toEqualTypeOf<
|
||||
Array<QueryObserverResult<string, Error>>
|
||||
>()
|
||||
|
||||
const query2 = useQueries({ queries: queriesWithoutSelect })
|
||||
expectTypeOf(query2.value).toEqualTypeOf<
|
||||
Array<QueryObserverResult<number, Error>>
|
||||
>()
|
||||
|
||||
const query3 = useQueries({ queries: queriesWithQueryOptions })
|
||||
expectTypeOf(query3.value).toEqualTypeOf<
|
||||
Array<QueryObserverResult<string, Error>>
|
||||
>()
|
||||
|
||||
const query4 = useQueries({ queries: queriesWithQueryOptionsWithoutSelect })
|
||||
expectTypeOf(query4.value).toEqualTypeOf<
|
||||
Array<QueryObserverResult<number, Error>>
|
||||
>()
|
||||
|
||||
const queryCombine = useQueries({
|
||||
queries: queries,
|
||||
combine: (data) => {
|
||||
return data.reduce((acc, i) => {
|
||||
acc.push(i.data ?? '')
|
||||
return acc
|
||||
}, [] as Array<string>)
|
||||
},
|
||||
})
|
||||
expectTypeOf(queryCombine.value).toEqualTypeOf<Array<string>>()
|
||||
|
||||
const queryCombineWithoutSelect = useQueries({
|
||||
queries: queriesWithoutSelect,
|
||||
combine: (data) => {
|
||||
return data.reduce((acc, i) => {
|
||||
acc.push(i.data ?? 0)
|
||||
return acc
|
||||
}, [] as Array<number>)
|
||||
},
|
||||
})
|
||||
|
||||
expectTypeOf(queryCombineWithoutSelect.value).toEqualTypeOf<Array<number>>()
|
||||
})
|
||||
|
||||
it('should return correct data for dynamic queries with mixed result types', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const Queries1 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve(1),
|
||||
}),
|
||||
}
|
||||
const Queries2 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: key2,
|
||||
queryFn: () => Promise.resolve(true),
|
||||
}),
|
||||
}
|
||||
|
||||
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
|
||||
const { value: queriesState } = useQueries({
|
||||
queries: [...queries1List, { ...Queries2.get() }],
|
||||
})
|
||||
|
||||
expectTypeOf(queriesState).toEqualTypeOf<
|
||||
[
|
||||
...Array<QueryObserverResult<number, Error>>,
|
||||
QueryObserverResult<boolean, Error>,
|
||||
]
|
||||
>()
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<
|
||||
number | boolean | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('should infer correct data type from queryOptions without initialData in useQueries', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const { value: queriesState } = useQueries({ queries: [options] })
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer correct data type from queryOptions with select in useQueries', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
const { value: queriesState } = useQueries({ queries: [options] })
|
||||
|
||||
expectTypeOf(queriesState[0].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
})
|
||||
508
packages/vue-query/src/__tests__/useQueries.test.ts
Normal file
508
packages/vue-query/src/__tests__/useQueries.test.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { onScopeDispose, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { useQueries } from '../useQueries'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
|
||||
describe('useQueries', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return result for each query', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
]
|
||||
const queriesState = useQueries({ queries })
|
||||
|
||||
expect(queriesState.value).toMatchObject([
|
||||
{
|
||||
status: 'pending',
|
||||
isPending: true,
|
||||
isFetching: true,
|
||||
isStale: true,
|
||||
},
|
||||
{
|
||||
status: 'pending',
|
||||
isPending: true,
|
||||
isFetching: true,
|
||||
isStale: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should resolve to success and update reactive state', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
]
|
||||
const queriesState = useQueries({ queries })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queriesState.value).toMatchObject([
|
||||
{
|
||||
status: 'success',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should reject one of the queries and update reactive state', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () =>
|
||||
sleep(0).then(() => Promise.reject(new Error('Some error'))),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
]
|
||||
const queriesState = useQueries({ queries })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queriesState.value).toMatchObject([
|
||||
{
|
||||
status: 'error',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should return state for new queries', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
const key4 = queryKey()
|
||||
const queries = ref([
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'value31'),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'value32'),
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => sleep(0).then(() => 'value33'),
|
||||
},
|
||||
])
|
||||
const queriesState = useQueries({ queries })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
queries.value.splice(
|
||||
0,
|
||||
queries.value.length,
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'value31'),
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => sleep(0).then(() => 'value34'),
|
||||
},
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queriesState.value.length).toEqual(2)
|
||||
expect(queriesState.value).toMatchObject([
|
||||
{
|
||||
data: 'value31',
|
||||
status: 'success',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
{
|
||||
data: 'value34',
|
||||
status: 'success',
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isStale: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should stop listening to changes on onScopeDispose', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const onScopeDisposeMock = onScopeDispose as MockedFunction<
|
||||
typeof onScopeDispose
|
||||
>
|
||||
onScopeDisposeMock.mockImplementationOnce((fn) => fn())
|
||||
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
]
|
||||
const queriesState = useQueries({ queries })
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queriesState.value).toMatchObject([
|
||||
{
|
||||
status: 'pending',
|
||||
isPending: true,
|
||||
isFetching: true,
|
||||
isStale: true,
|
||||
},
|
||||
{
|
||||
status: 'pending',
|
||||
isPending: true,
|
||||
isFetching: true,
|
||||
isStale: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should use queryClient provided via options', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queryClient = new QueryClient()
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
},
|
||||
]
|
||||
|
||||
useQueries({ queries }, queryClient)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(useQueryClient).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should combine queries', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const firstResult = 'first result'
|
||||
const secondResult = 'second result'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queries = [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => firstResult),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(0).then(() => secondResult),
|
||||
},
|
||||
]
|
||||
|
||||
const queriesResult = useQueries(
|
||||
{
|
||||
queries,
|
||||
combine: (results) => {
|
||||
return {
|
||||
combined: true,
|
||||
res: results.map((res) => res.data),
|
||||
}
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queriesResult.value).toEqual({
|
||||
combined: true,
|
||||
res: [firstResult, secondResult],
|
||||
})
|
||||
})
|
||||
|
||||
it('should be `enabled` to accept getter function', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const checked = ref(false)
|
||||
|
||||
useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn: fetchFn,
|
||||
enabled: () => checked.value,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(fetchFn).not.toHaveBeenCalled()
|
||||
|
||||
checked.value = true
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow getters for query keys', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const key1 = ref('key1')
|
||||
const key2 = ref('key2')
|
||||
|
||||
useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: [...key, () => key1.value, () => key2.value],
|
||||
queryFn: fetchFn,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
key1.value = 'key3'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
|
||||
key2.value = 'key4'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should allow arbitrarily nested getters for query keys', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const key1 = ref('key1')
|
||||
const key2 = ref('key2')
|
||||
const key3 = ref('key3')
|
||||
const key4 = ref('key4')
|
||||
const key5 = ref('key5')
|
||||
|
||||
useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: [
|
||||
...key,
|
||||
key1,
|
||||
() => key2.value,
|
||||
{ key: () => key3.value },
|
||||
[{ foo: { bar: () => key4.value } }],
|
||||
() => ({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: () => key5.value,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFn: fetchFn,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
key1.value = 'key1-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
|
||||
key2.value = 'key2-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3)
|
||||
|
||||
key3.value = 'key3-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(4)
|
||||
|
||||
key4.value = 'key4-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(5)
|
||||
|
||||
key5.value = 'key5-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
|
||||
it('should refetch only the specific query without affecting others', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
let userCount = 0
|
||||
let postCount = 0
|
||||
|
||||
const queriesState = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => `users-${++userCount}`),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(20).then(() => `posts-${++postCount}`),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
|
||||
expect(queriesState.value[0].data).toBe('users-1')
|
||||
expect(queriesState.value[1].data).toBe('posts-1')
|
||||
|
||||
queriesState.value[0].refetch()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(queriesState.value[0].data).toBe('users-2')
|
||||
expect(queriesState.value[1].data).toBe('posts-1')
|
||||
})
|
||||
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => 'data'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const key1 = ref('key1')
|
||||
const key2 = ref('key2')
|
||||
const key3 = ref('key3')
|
||||
const key4 = ref('key4')
|
||||
const key5 = ref('key5')
|
||||
|
||||
useQueries({
|
||||
queries: () => [
|
||||
{
|
||||
queryKey: [
|
||||
...key,
|
||||
key1,
|
||||
key2.value,
|
||||
{ key: key3.value },
|
||||
[{ foo: { bar: key4.value } }],
|
||||
() => ({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: key5.value,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFn: fetchFn,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
key1.value = 'key1-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
|
||||
key2.value = 'key2-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3)
|
||||
|
||||
key3.value = 'key3-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(4)
|
||||
|
||||
key4.value = 'key4-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(5)
|
||||
|
||||
key5.value = 'key5-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
})
|
||||
428
packages/vue-query/src/__tests__/useQuery.test-d.ts
Normal file
428
packages/vue-query/src/__tests__/useQuery.test-d.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { computed, reactive, ref } from 'vue-demi'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { queryOptions, skipToken, useQuery } from '..'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type { OmitKeyof, UseQueryOptions, UseQueryReturnType } from '..'
|
||||
|
||||
describe('useQuery', () => {
|
||||
describe('Config object overload', () => {
|
||||
it('TData should always be defined when initialData is provided as an object', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should be defined when passed through queryOptions', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: true,
|
||||
},
|
||||
})
|
||||
const { data } = reactive(useQuery(options))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(1),
|
||||
})
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
...options,
|
||||
select: (data) => data > 1,
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(query.data).toEqualTypeOf<boolean | undefined>()
|
||||
})
|
||||
|
||||
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => ({
|
||||
wow: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data, isSuccess } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
if (isSuccess) {
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should not have undefined when initialData is provided', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
initialData: 42,
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('generic queryFn', () => {
|
||||
it('should infer the result type from a generic query function', () => {
|
||||
const key = queryKey()
|
||||
|
||||
function queryFn<T = string>(): Promise<T> {
|
||||
return Promise.resolve({} as T)
|
||||
}
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => queryFn(),
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(query.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error | null>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('generic queryKey inference (#8199)', () => {
|
||||
it('should not error when wrapping useQuery in a composable that propagates a generic type to the queryKey', () => {
|
||||
const basket = { fruit: 'apple', vegetable: 'broccoli' } as const
|
||||
|
||||
function getBasket<T extends 'fruit' | 'vegetable'>(type: T) {
|
||||
return basket[type]
|
||||
}
|
||||
|
||||
function useBasket<T extends 'fruit' | 'vegetable'>(type: T) {
|
||||
return useQuery({
|
||||
queryKey: ['basket', type] as const,
|
||||
queryFn({ queryKey: [, t] }) {
|
||||
return getBasket(t)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
assertType<UseQueryReturnType<'apple' | 'broccoli', Error>>(
|
||||
useBasket('fruit'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom composable', () => {
|
||||
it('should allow custom composable using UseQueryOptions', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const useCustomQuery = (
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<string>,
|
||||
'queryKey' | 'queryFn',
|
||||
'safely'
|
||||
>,
|
||||
) => {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
})
|
||||
}
|
||||
|
||||
const { data } = reactive(useCustomQuery())
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('structuralSharing', () => {
|
||||
it('should be able to use structuralSharing with unknown types', () => {
|
||||
const key = queryKey()
|
||||
|
||||
// https://github.com/TanStack/query/issues/6525#issuecomment-1938411343
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => 5,
|
||||
structuralSharing: (oldData, newData) => {
|
||||
expectTypeOf(oldData).toBeUnknown()
|
||||
expectTypeOf(newData).toBeUnknown()
|
||||
return newData
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Discriminated union return type', () => {
|
||||
it('data should be possibly undefined by default', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(query.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('data should be defined when query is success', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be null when query is success', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.error).toEqualTypeOf<null>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should be undefined when query is pending', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isPending) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<undefined>()
|
||||
}
|
||||
})
|
||||
|
||||
it('error should be defined when query is error', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = reactive(
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isError) {
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error>()
|
||||
}
|
||||
})
|
||||
|
||||
it('data should be a union of refs without reactive()', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
expectTypeOf(query.data).toEqualTypeOf<Ref<string> | Ref<undefined>>()
|
||||
})
|
||||
|
||||
it('data.value should narrow on an undefined check without reactive()', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
if (data.value !== undefined) {
|
||||
expectTypeOf(data.value).toEqualTypeOf<string>()
|
||||
expectTypeOf(data).toEqualTypeOf<Ref<string>>()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('accept ref options', () => {
|
||||
it('should accept ref options', () => {
|
||||
const options = ref({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
const query = reactive(useQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should accept computed options', () => {
|
||||
const options = computed(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}))
|
||||
|
||||
const query = reactive(useQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
|
||||
it('should accept computed query options', () => {
|
||||
const options = computed(() =>
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
|
||||
const query = reactive(useQuery(options))
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('skipToken', () => {
|
||||
it('should accept skipToken inside a whole-options getter', () => {
|
||||
const id = ref<string | null>('1')
|
||||
|
||||
const query = reactive(
|
||||
useQuery(() => {
|
||||
const current = id.value
|
||||
return {
|
||||
queryKey: ['post', current],
|
||||
queryFn: current
|
||||
? () => sleep(0).then(() => 'Some data')
|
||||
: skipToken,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
if (query.isSuccess) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryKey reactivity rules', () => {
|
||||
it('should accept a bare reactive getter for the whole queryKey array', () => {
|
||||
const id = ref(1)
|
||||
assertType(
|
||||
useQuery({
|
||||
queryKey: () => ['post', id.value],
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('select', () => {
|
||||
it('should narrow data to the type select returns', () => {
|
||||
const { data } = useQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => ['a', 'b', 'c']),
|
||||
select: (posts) => posts.length,
|
||||
})
|
||||
|
||||
expectTypeOf(data.value).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
})
|
||||
})
|
||||
781
packages/vue-query/src/__tests__/useQuery.test.ts
Normal file
781
packages/vue-query/src/__tests__/useQuery.test.ts
Normal file
@@ -0,0 +1,781 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
onScopeDispose,
|
||||
reactive,
|
||||
ref,
|
||||
} from 'vue-demi'
|
||||
import { QueryObserver, experimental_streamedQuery } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { keepPreviousData } from '..'
|
||||
import { useQuery } from '../useQuery'
|
||||
import { useBaseQuery } from '../useBaseQuery'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
import type { Mock, MockedFunction } from 'vitest'
|
||||
|
||||
vi.mock('../useQueryClient')
|
||||
vi.mock('../useBaseQuery')
|
||||
|
||||
describe('useQuery', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should properly execute query', () => {
|
||||
const key = queryKey()
|
||||
const queryFn = () => sleep(0).then(() => 'Some data')
|
||||
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
staleTime: 1000,
|
||||
})
|
||||
|
||||
expect(useBaseQuery).toHaveBeenCalledWith(
|
||||
QueryObserver,
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
staleTime: 1000,
|
||||
},
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it('should work with options getter', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'result01'),
|
||||
}))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result01' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should work with options getter and be reactive', async () => {
|
||||
const key = queryKey()
|
||||
const keyRef = ref('key011')
|
||||
const resultRef = ref('result02')
|
||||
const query = useQuery(() => ({
|
||||
queryKey: [...key, keyRef.value],
|
||||
queryFn: () => sleep(0).then(() => resultRef.value),
|
||||
}))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result02' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
|
||||
resultRef.value = 'result021'
|
||||
keyRef.value = 'key012'
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result021' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should return pending status initially', () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'pending' },
|
||||
isPending: { value: true },
|
||||
isFetching: { value: true },
|
||||
isStale: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve to success and update reactive state: useQuery(key, dataFn)', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'result2'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result2' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve to success and update reactive state: useQuery(optionsObj)', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'result31'),
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result31' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve to success and update reactive state: useQuery(key, optionsObj)', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'result32'),
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'result32' },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isSuccess: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject and update reactive state', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(0).then(() => Promise.reject(new Error('Some error'))),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'error' },
|
||||
data: { value: undefined },
|
||||
error: { value: { message: 'Some error' } },
|
||||
isPending: { value: false },
|
||||
isFetching: { value: false },
|
||||
isFetched: { value: true },
|
||||
isError: { value: true },
|
||||
failureCount: { value: 1 },
|
||||
failureReason: { value: { message: 'Some error' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('should update query on reactive (Ref) key change', async () => {
|
||||
const key = queryKey()
|
||||
const secondKeyRef = ref('key7')
|
||||
const query = useQuery({
|
||||
queryKey: [...key, secondKeyRef],
|
||||
queryFn: () => sleep(10).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
})
|
||||
|
||||
secondKeyRef.value = 'key8'
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'pending' },
|
||||
data: { value: undefined },
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
})
|
||||
})
|
||||
|
||||
it("should update query when an option is passed as Ref and it's changed", async () => {
|
||||
const key = queryKey()
|
||||
const enabled = ref(false)
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'Some data'),
|
||||
enabled,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
fetchStatus: { value: 'idle' },
|
||||
data: { value: undefined },
|
||||
})
|
||||
|
||||
enabled.value = true
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
fetchStatus: { value: 'fetching' },
|
||||
data: { value: undefined },
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly execute dependent queries', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const { data } = useQuery({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
const enabled = computed(() => !!data.value)
|
||||
|
||||
const dependentQueryFn = vi
|
||||
.fn()
|
||||
.mockImplementation(() => sleep(10).then(() => 'Some data'))
|
||||
const { fetchStatus, status } = useQuery(
|
||||
reactive({
|
||||
queryKey: key2,
|
||||
queryFn: dependentQueryFn,
|
||||
enabled,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(data.value).toStrictEqual(undefined)
|
||||
expect(fetchStatus.value).toStrictEqual('idle')
|
||||
expect(dependentQueryFn).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(data.value).toStrictEqual('Some data')
|
||||
expect(fetchStatus.value).toStrictEqual('fetching')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fetchStatus.value).toStrictEqual('idle')
|
||||
expect(status.value).toStrictEqual('success')
|
||||
expect(dependentQueryFn).toHaveBeenCalledTimes(1)
|
||||
expect(dependentQueryFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: key2 }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should stop listening to changes on onScopeDispose', async () => {
|
||||
const key = queryKey()
|
||||
const onScopeDisposeMock = onScopeDispose as MockedFunction<
|
||||
typeof onScopeDispose
|
||||
>
|
||||
onScopeDisposeMock.mockImplementationOnce((fn) => fn())
|
||||
|
||||
const { status } = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(status.value).toStrictEqual('pending')
|
||||
})
|
||||
|
||||
it('should use the current value for the queryKey when refetch is called', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const keyRef = ref('key11')
|
||||
const query = useQuery({
|
||||
queryKey: [...key, keyRef],
|
||||
queryFn: fetchFn,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
expect(fetchFn).not.toHaveBeenCalled()
|
||||
await query.refetch()
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
expect(fetchFn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
queryKey: [...key, 'key11'],
|
||||
}),
|
||||
)
|
||||
|
||||
keyRef.value = 'key12'
|
||||
await query.refetch()
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
expect(fetchFn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
queryKey: [...key, 'key12'],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should be `enabled` to accept getter function', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const checked = ref(false)
|
||||
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: fetchFn,
|
||||
enabled: () => checked.value,
|
||||
})
|
||||
|
||||
expect(fetchFn).not.toHaveBeenCalled()
|
||||
|
||||
checked.value = true
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow getters for query keys', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const key1 = ref('key1')
|
||||
const key2 = ref('key2')
|
||||
|
||||
useQuery({
|
||||
queryKey: [...key, () => key1.value, () => key2.value],
|
||||
queryFn: fetchFn,
|
||||
})
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
key1.value = 'key3'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
|
||||
key2.value = 'key4'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should allow arbitrarily nested getters for query keys', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => 'foo')
|
||||
const key1 = ref('key1')
|
||||
const key2 = ref('key2')
|
||||
const key3 = ref('key3')
|
||||
const key4 = ref('key4')
|
||||
const key5 = ref('key5')
|
||||
|
||||
useQuery({
|
||||
queryKey: [
|
||||
...key,
|
||||
key1,
|
||||
() => key2.value,
|
||||
{ key: () => key3.value },
|
||||
[{ foo: { bar: () => key4.value } }],
|
||||
() => ({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: () => key5.value,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFn: fetchFn,
|
||||
})
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
key1.value = 'key1-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2)
|
||||
|
||||
key2.value = 'key2-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3)
|
||||
|
||||
key3.value = 'key3-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(4)
|
||||
|
||||
key4.value = 'key4-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(5)
|
||||
|
||||
key5.value = 'key5-updated'
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
|
||||
it('should derive data via select without changing what is cached', async () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => ['a', 'b', 'c']),
|
||||
select: (posts) => posts.length,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 3 },
|
||||
})
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
expect(queryClient.getQueryData(key)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('should stay disabled until the dependent value is set', async () => {
|
||||
const key = queryKey()
|
||||
const postId = ref<number>()
|
||||
const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data'))
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [...key, postId],
|
||||
queryFn: fetchFn,
|
||||
enabled: () => postId.value != null,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fetchFn).not.toHaveBeenCalled()
|
||||
expect(query).toMatchObject({ status: { value: 'pending' } })
|
||||
|
||||
postId.value = 1
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'Some data' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should seed from initialData and skip the loading state', () => {
|
||||
const key = queryKey()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'fetched data'),
|
||||
initialData: 'seeded data',
|
||||
})
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'seeded data' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should still fetch in the background and replace initialData with the fetched value', async () => {
|
||||
const key = queryKey()
|
||||
const fetchFn = vi.fn(() => sleep(10).then(() => 'fetched data'))
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: fetchFn,
|
||||
initialData: 'seeded data',
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1)
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'success' },
|
||||
data: { value: 'fetched data' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep the previous page visible while the next page loads with keepPreviousData', async () => {
|
||||
const key = queryKey()
|
||||
const page = ref(0)
|
||||
const fetchFn = vi.fn((pageParam: number) =>
|
||||
sleep(10).then(() => `page-${pageParam}`),
|
||||
)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [...key, page],
|
||||
queryFn: () => fetchFn(page.value),
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
data: { value: 'page-0' },
|
||||
isPlaceholderData: { value: false },
|
||||
})
|
||||
|
||||
page.value = 1
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
data: { value: 'page-0' },
|
||||
isPlaceholderData: { value: true },
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
data: { value: 'page-1' },
|
||||
isPlaceholderData: { value: false },
|
||||
})
|
||||
})
|
||||
|
||||
describe('throwOnError', () => {
|
||||
it('should evaluate throwOnError when query is expected to throw', async () => {
|
||||
const key = queryKey()
|
||||
const boundaryFn = vi.fn()
|
||||
useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(0).then(() => Promise.reject(new Error('Some error'))),
|
||||
retry: false,
|
||||
throwOnError: boundaryFn,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(boundaryFn).toHaveBeenCalledTimes(1)
|
||||
expect(boundaryFn).toHaveBeenCalledWith(
|
||||
Error('Some error'),
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({ status: 'error' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('outside scope warning', () => {
|
||||
it('should warn when used outside of setup function in development mode', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development')
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
useQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(0).then(() => 'data'),
|
||||
})
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
} finally {
|
||||
warnSpy.mockRestore()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('suspense', () => {
|
||||
it('should return a Promise', () => {
|
||||
const key = queryKey()
|
||||
const getCurrentInstanceSpy = getCurrentInstance as Mock
|
||||
getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
})
|
||||
const result = query.suspense()
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it('should resolve after being enabled', async () => {
|
||||
const key = queryKey()
|
||||
const getCurrentInstanceSpy = getCurrentInstance as Mock
|
||||
getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
|
||||
|
||||
let afterTimeout = false
|
||||
const isEnabled = ref(false)
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
enabled: isEnabled,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
afterTimeout = true
|
||||
isEnabled.value = true
|
||||
}, 200)
|
||||
|
||||
query.suspense()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
|
||||
expect(afterTimeout).toBe(true)
|
||||
})
|
||||
|
||||
it('should resolve immediately when stale without refetching', () => {
|
||||
const key = queryKey()
|
||||
const getCurrentInstanceSpy = getCurrentInstance as Mock
|
||||
getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
|
||||
|
||||
const fetcherSpy = vi.fn(() => sleep(0).then(() => 'Some data'))
|
||||
|
||||
// let afterTimeout = false;
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(0).then(() => 'Some data'),
|
||||
staleTime: 10000,
|
||||
initialData: 'foo',
|
||||
})
|
||||
|
||||
return query.suspense().then(() => {
|
||||
expect(fetcherSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw from suspense by default', async () => {
|
||||
const key = queryKey()
|
||||
const getCurrentInstanceSpy = getCurrentInstance as Mock
|
||||
getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(0).then(() => Promise.reject(new Error('Some error'))),
|
||||
staleTime: 10000,
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(query).toMatchObject({
|
||||
status: { value: 'error' },
|
||||
isError: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw from suspense when throwOnError is true', async () => {
|
||||
const key = queryKey()
|
||||
const getCurrentInstanceSpy = getCurrentInstance as Mock
|
||||
getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
|
||||
|
||||
const boundaryFn = vi.fn()
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(0).then(() => Promise.reject(new Error('Some error'))),
|
||||
staleTime: 10000,
|
||||
throwOnError: boundaryFn,
|
||||
})
|
||||
|
||||
query.suspense()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
|
||||
expect(boundaryFn).toHaveBeenCalledTimes(2)
|
||||
expect(boundaryFn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
Error('Some error'),
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({ status: 'error' }),
|
||||
}),
|
||||
)
|
||||
expect(boundaryFn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
Error('Some error'),
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({ status: 'error' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should release suspense when setQueryData is called while fetch is in-flight', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10000).then(() => 'fetched'),
|
||||
})
|
||||
|
||||
const suspensePromise = query.suspense()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
queryClient.setQueryData(key, 'manual data')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
const result = await suspensePromise
|
||||
expect(result.data).toBe('manual data')
|
||||
})
|
||||
|
||||
it('should release suspense when streamedQuery receives first chunk', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
async function* numberGenerator() {
|
||||
await sleep(10)
|
||||
yield 'chunk1'
|
||||
await sleep(10)
|
||||
yield 'chunk2'
|
||||
}
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: key,
|
||||
queryFn: experimental_streamedQuery({
|
||||
streamFn: () => numberGenerator(),
|
||||
}),
|
||||
})
|
||||
|
||||
const suspensePromise = query.suspense()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
const result = await suspensePromise
|
||||
expect(result.data).toStrictEqual(['chunk1'])
|
||||
})
|
||||
})
|
||||
})
|
||||
63
packages/vue-query/src/__tests__/useQueryClient.test.ts
Normal file
63
packages/vue-query/src/__tests__/useQueryClient.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as VueDemi from 'vue-demi'
|
||||
import { useQueryClient } from '../useQueryClient'
|
||||
import { VUE_QUERY_CLIENT } from '../utils'
|
||||
|
||||
vi.mock('vue-demi', async () => {
|
||||
const actual = await vi.importActual<typeof VueDemi>('vue-demi')
|
||||
return {
|
||||
...actual,
|
||||
inject: vi.fn(),
|
||||
hasInjectionContext: vi.fn(() => true),
|
||||
}
|
||||
})
|
||||
|
||||
describe('useQueryClient', () => {
|
||||
const injectSpy = vi.mocked(VueDemi.inject)
|
||||
const hasInjectionContextSpy = vi.mocked(VueDemi.hasInjectionContext)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return queryClient when it is provided in the context', () => {
|
||||
const queryClientMock = { name: 'Mocked client' }
|
||||
injectSpy.mockReturnValueOnce(queryClientMock)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
expect(queryClient).toStrictEqual(queryClientMock)
|
||||
expect(injectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT)
|
||||
})
|
||||
|
||||
it('should throw an error when queryClient does not exist in the context', () => {
|
||||
injectSpy.mockReturnValueOnce(undefined)
|
||||
|
||||
expect(useQueryClient).toThrow(
|
||||
"No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.",
|
||||
)
|
||||
expect(injectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT)
|
||||
})
|
||||
|
||||
it('should throw an error when used outside of setup function', () => {
|
||||
hasInjectionContextSpy.mockReturnValueOnce(false)
|
||||
|
||||
expect(useQueryClient).toThrow(
|
||||
'vue-query hooks can only be used inside setup() function or functions that support injection context.',
|
||||
)
|
||||
expect(hasInjectionContextSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call inject with a custom key as a suffix', () => {
|
||||
const queryClientKey = 'foo'
|
||||
const expectedKeyParameter = `${VUE_QUERY_CLIENT}:${queryClientKey}`
|
||||
const queryClientMock = { name: 'Mocked client' }
|
||||
injectSpy.mockReturnValueOnce(queryClientMock)
|
||||
|
||||
useQueryClient(queryClientKey)
|
||||
|
||||
expect(injectSpy).toHaveBeenCalledWith(expectedKeyParameter)
|
||||
})
|
||||
})
|
||||
155
packages/vue-query/src/__tests__/utils.test.ts
Normal file
155
packages/vue-query/src/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { reactive, ref } from 'vue-demi'
|
||||
import { cloneDeep, cloneDeepUnref, updateState } from '../utils'
|
||||
|
||||
describe('utils', () => {
|
||||
describe('updateState', () => {
|
||||
it('should update first object with values from the second one', () => {
|
||||
const origin = { option1: 'a', option2: 'b', option3: 'c' }
|
||||
const update = { option1: 'x', option2: 'y', option3: 'z' }
|
||||
const expected = { option1: 'x', option2: 'y', option3: 'z' }
|
||||
|
||||
updateState(origin, update)
|
||||
expect(origin).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should update only existing keys', () => {
|
||||
const origin = { option1: 'a', option2: 'b' }
|
||||
const update = { option1: 'x', option2: 'y', option3: 'z' }
|
||||
const expected = { option1: 'x', option2: 'y' }
|
||||
|
||||
updateState(origin, update)
|
||||
expect(origin).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should remove non existing keys', () => {
|
||||
const origin = { option1: 'a', option2: 'b', option3: 'c' }
|
||||
const update = { option1: 'x', option2: 'y' }
|
||||
const expected = { option1: 'x', option2: 'y' }
|
||||
|
||||
updateState(origin, update)
|
||||
expect(origin).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloneDeep', () => {
|
||||
it('should copy primitives and functions AS-IS', () => {
|
||||
expect(cloneDeep(3456)).toBe(3456)
|
||||
expect(cloneDeep('theString')).toBe('theString')
|
||||
expect(cloneDeep(null)).toBe(null)
|
||||
})
|
||||
|
||||
it('should copy Maps and Sets AS-IS', () => {
|
||||
const setVal = new Set([3, 4, 5])
|
||||
const setValCopy = cloneDeep(setVal)
|
||||
expect(setValCopy).toBe(setVal)
|
||||
expect(setValCopy).toStrictEqual(new Set([3, 4, 5]))
|
||||
|
||||
const mapVal = new Map([
|
||||
['a', 'aVal'],
|
||||
['b', 'bVal'],
|
||||
])
|
||||
const mapValCopy = cloneDeep(mapVal)
|
||||
expect(mapValCopy).toBe(mapVal)
|
||||
expect(mapValCopy).toStrictEqual(
|
||||
new Map([
|
||||
['a', 'aVal'],
|
||||
['b', 'bVal'],
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should deeply copy arrays', () => {
|
||||
const val = [
|
||||
25,
|
||||
'str',
|
||||
null,
|
||||
new Set([3, 4]),
|
||||
[5, 6, { a: 1 }],
|
||||
undefined,
|
||||
]
|
||||
const cp = cloneDeep(val)
|
||||
expect(cp).toStrictEqual([
|
||||
25,
|
||||
'str',
|
||||
null,
|
||||
new Set([3, 4]),
|
||||
[5, 6, { a: 1 }],
|
||||
undefined,
|
||||
])
|
||||
expect(cp).not.toBe(val)
|
||||
expect(cp[3]).toBe(val[3]) // Set([3, 4])
|
||||
expect(cp[4]).not.toBe(val[4]) // [5, 6, { a: 1 }]
|
||||
expect((cp[4] as Array<number>)[2]).not.toBe((val[4] as Array<number>)[2]) // { a : 1 }
|
||||
})
|
||||
|
||||
it('should deeply copy object', () => {
|
||||
const val = reactive({
|
||||
a: 25,
|
||||
b: 'str',
|
||||
c: null,
|
||||
d: undefined,
|
||||
e: new Set([5, 6]),
|
||||
f: [3, 4],
|
||||
g: { fa: 26 },
|
||||
})
|
||||
const cp = cloneDeep(val)
|
||||
|
||||
expect(cp).toStrictEqual({
|
||||
a: 25,
|
||||
b: 'str',
|
||||
c: null,
|
||||
d: undefined,
|
||||
e: new Set([5, 6]),
|
||||
f: [3, 4],
|
||||
g: { fa: 26 },
|
||||
})
|
||||
|
||||
expect(cp.e).toBe(val.e) // Set
|
||||
expect(cp.f).not.toBe(val.f) // []
|
||||
expect(cp.g).not.toBe(val.g) // {}
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloneDeepUnref', () => {
|
||||
it('should unref primitives', () => {
|
||||
expect(cloneDeepUnref(ref(34))).toBe(34)
|
||||
expect(cloneDeepUnref(ref('myStr'))).toBe('myStr')
|
||||
})
|
||||
|
||||
it('should deeply unref arrays', () => {
|
||||
const val = ref([2, 3, ref(4), ref('5'), { a: ref(6) }, [ref(7)]])
|
||||
const cp = cloneDeepUnref(val)
|
||||
expect(cp).toStrictEqual([2, 3, 4, '5', { a: 6 }, [7]])
|
||||
})
|
||||
|
||||
it('should deeply unref objects', () => {
|
||||
const val = ref({
|
||||
a: 1,
|
||||
b: ref(2),
|
||||
c: [ref('c1'), ref(['c2'])],
|
||||
d: {
|
||||
e: ref('e'),
|
||||
},
|
||||
})
|
||||
const cp = cloneDeepUnref(val)
|
||||
|
||||
expect(cp).toEqual({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: ['c1', ['c2']],
|
||||
d: { e: 'e' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should clone getters returning values in queryKey', () => {
|
||||
const val = ref({ queryKey: [1, 2, () => '3'] })
|
||||
const cp = cloneDeepUnref(val)
|
||||
expect(cp).toStrictEqual({ queryKey: [1, 2, '3'] })
|
||||
})
|
||||
|
||||
it('should unref undefined', () => {
|
||||
expect(cloneDeepUnref(ref(undefined))).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
398
packages/vue-query/src/__tests__/vueQueryPlugin.test.ts
Normal file
398
packages/vue-query/src/__tests__/vueQueryPlugin.test.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isVue2, isVue3, ref } from 'vue-demi'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { QueryClient } from '../queryClient'
|
||||
import { VueQueryPlugin } from '../vueQueryPlugin'
|
||||
import { VUE_QUERY_CLIENT } from '../utils'
|
||||
import { setupDevtools } from '../devtools/devtools'
|
||||
import { useQuery } from '../useQuery'
|
||||
import { useQueries } from '../useQueries'
|
||||
import type { App, ComponentOptions } from 'vue'
|
||||
import type { Mock } from 'vitest'
|
||||
|
||||
vi.mock('../devtools/devtools')
|
||||
vi.mock('../useQueryClient')
|
||||
vi.mock('../useBaseQuery')
|
||||
|
||||
type UnmountCallback = () => void
|
||||
|
||||
interface TestApp extends App {
|
||||
onUnmount: UnmountCallback
|
||||
_unmount: UnmountCallback
|
||||
_mixin: ComponentOptions
|
||||
_provided: Record<string, any>
|
||||
$root: TestApp
|
||||
}
|
||||
|
||||
const itIf = (condition: boolean) => (condition ? it : it.skip)
|
||||
|
||||
function getAppMock(withUnmountHook = false): TestApp {
|
||||
const mock = {
|
||||
provide: vi.fn(),
|
||||
unmount: vi.fn(),
|
||||
onUnmount: withUnmountHook
|
||||
? vi.fn((u: UnmountCallback) => {
|
||||
mock._unmount = u
|
||||
})
|
||||
: undefined,
|
||||
mixin: (m: ComponentOptions) => {
|
||||
mock._mixin = m
|
||||
},
|
||||
} as unknown as TestApp
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
describe('VueQueryPlugin', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('devtools', () => {
|
||||
it('should NOT setup devtools', () => {
|
||||
const setupDevtoolsMock = setupDevtools as Mock
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock)
|
||||
|
||||
expect(setupDevtoolsMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
itIf(isVue2)('should NOT setup devtools by default', () => {
|
||||
const envCopy = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
const setupDevtoolsMock = setupDevtools as Mock
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock)
|
||||
|
||||
appMock.$root = appMock
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
process.env.NODE_ENV = envCopy
|
||||
|
||||
expect(setupDevtoolsMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
itIf(isVue2)('should setup devtools', () => {
|
||||
const envCopy = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
const setupDevtoolsMock = setupDevtools as Mock
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true })
|
||||
|
||||
appMock.$root = appMock
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
process.env.NODE_ENV = envCopy
|
||||
|
||||
expect(setupDevtoolsMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
itIf(isVue3)('should NOT setup devtools by default', () => {
|
||||
const envCopy = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
const setupDevtoolsMock = setupDevtools as Mock
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock)
|
||||
process.env.NODE_ENV = envCopy
|
||||
|
||||
expect(setupDevtoolsMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
itIf(isVue3)('should setup devtools', () => {
|
||||
const envCopy = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
const setupDevtoolsMock = setupDevtools as Mock
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true })
|
||||
process.env.NODE_ENV = envCopy
|
||||
|
||||
expect(setupDevtoolsMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when app unmounts', () => {
|
||||
it('should call unmount on each client when onUnmount is missing', () => {
|
||||
const appMock = getAppMock()
|
||||
const customClient = {
|
||||
mount: vi.fn(),
|
||||
unmount: vi.fn(),
|
||||
} as unknown as QueryClient
|
||||
const originalUnmount = appMock.unmount
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClient: customClient,
|
||||
})
|
||||
|
||||
appMock.unmount()
|
||||
|
||||
expect(appMock.unmount).not.toEqual(originalUnmount)
|
||||
expect(customClient.unmount).toHaveBeenCalledTimes(1)
|
||||
expect(originalUnmount).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onUnmount if present', () => {
|
||||
const appMock = getAppMock(true)
|
||||
const customClient = {
|
||||
mount: vi.fn(),
|
||||
unmount: vi.fn(),
|
||||
} as unknown as QueryClient
|
||||
const originalUnmount = appMock.unmount
|
||||
VueQueryPlugin.install(appMock, { queryClient: customClient })
|
||||
|
||||
appMock._unmount()
|
||||
|
||||
expect(appMock.unmount).toEqual(originalUnmount)
|
||||
expect(customClient.unmount).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when called without additional options', () => {
|
||||
itIf(isVue2)('should provide a client with default clientKey', () => {
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock)
|
||||
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
|
||||
expect(appMock._provided).toMatchObject({
|
||||
VUE_QUERY_CLIENT: expect.any(QueryClient),
|
||||
})
|
||||
})
|
||||
|
||||
itIf(isVue3)('should provide a client with default clientKey', () => {
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock)
|
||||
|
||||
expect(appMock.provide).toHaveBeenCalledWith(
|
||||
VUE_QUERY_CLIENT,
|
||||
expect.any(QueryClient),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when called with custom clientKey', () => {
|
||||
itIf(isVue2)('should provide a client with customized clientKey', () => {
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' })
|
||||
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
|
||||
expect(appMock._provided).toMatchObject({
|
||||
[VUE_QUERY_CLIENT + ':CUSTOM']: expect.any(QueryClient),
|
||||
})
|
||||
})
|
||||
|
||||
itIf(isVue3)('should provide a client with customized clientKey', () => {
|
||||
const appMock = getAppMock()
|
||||
VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' })
|
||||
|
||||
expect(appMock.provide).toHaveBeenCalledWith(
|
||||
VUE_QUERY_CLIENT + ':CUSTOM',
|
||||
expect.any(QueryClient),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when called with custom client', () => {
|
||||
itIf(isVue2)('should provide that custom client', () => {
|
||||
const appMock = getAppMock()
|
||||
const customClient = { mount: vi.fn() } as unknown as QueryClient
|
||||
VueQueryPlugin.install(appMock, { queryClient: customClient })
|
||||
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
|
||||
expect(customClient.mount).toHaveBeenCalled()
|
||||
expect(appMock._provided).toMatchObject({
|
||||
VUE_QUERY_CLIENT: customClient,
|
||||
})
|
||||
})
|
||||
|
||||
itIf(isVue3)('should provide that custom client', () => {
|
||||
const appMock = getAppMock()
|
||||
const customClient = { mount: vi.fn() } as unknown as QueryClient
|
||||
VueQueryPlugin.install(appMock, { queryClient: customClient })
|
||||
|
||||
expect(customClient.mount).toHaveBeenCalled()
|
||||
expect(appMock.provide).toHaveBeenCalledWith(
|
||||
VUE_QUERY_CLIENT,
|
||||
customClient,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when called with custom client config', () => {
|
||||
itIf(isVue2)('should instantiate a client with the provided config', () => {
|
||||
const appMock = getAppMock()
|
||||
const config = {
|
||||
defaultOptions: { queries: { enabled: true } },
|
||||
}
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClientConfig: config,
|
||||
})
|
||||
|
||||
appMock._mixin.beforeCreate?.call(appMock)
|
||||
const client = appMock._provided.VUE_QUERY_CLIENT as QueryClient
|
||||
const defaultOptions = client.getDefaultOptions()
|
||||
|
||||
expect(defaultOptions).toEqual(config.defaultOptions)
|
||||
})
|
||||
|
||||
itIf(isVue3)('should instantiate a client with the provided config', () => {
|
||||
const appMock = getAppMock()
|
||||
const config = {
|
||||
defaultOptions: { queries: { enabled: true } },
|
||||
}
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClientConfig: config,
|
||||
})
|
||||
|
||||
const client = (appMock.provide as Mock).mock.calls[0]?.[1]
|
||||
const defaultOptions = client.getDefaultOptions()
|
||||
|
||||
expect(defaultOptions).toEqual(config.defaultOptions)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when persister is provided', () => {
|
||||
it('should properly modify isRestoring flag on queryClient', async () => {
|
||||
const appMock = getAppMock()
|
||||
const customClient = {
|
||||
mount: vi.fn(),
|
||||
isRestoring: ref(false),
|
||||
} as unknown as QueryClient
|
||||
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClient: customClient,
|
||||
clientPersister: () => [
|
||||
vi.fn(),
|
||||
new Promise((resolve) => {
|
||||
resolve()
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(true)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should delay useQuery subscription and not call fetcher if data is not stale', async () => {
|
||||
const key = queryKey()
|
||||
const appMock = getAppMock()
|
||||
const customClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 60,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClient: customClient,
|
||||
clientPersister: (client) => [
|
||||
vi.fn(),
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
client.setQueryData(key, () => ({
|
||||
foo: 'bar',
|
||||
}))
|
||||
resolve()
|
||||
}, 0)
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const fnSpy = vi.fn()
|
||||
|
||||
const query = useQuery(
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn: fnSpy,
|
||||
},
|
||||
customClient,
|
||||
)
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(true)
|
||||
expect(query.isFetching.value).toBe(false)
|
||||
expect(query.data.value).toStrictEqual(undefined)
|
||||
expect(fnSpy).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(false)
|
||||
expect(query.data.value).toStrictEqual({ foo: 'bar' })
|
||||
expect(fnSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should delay useQueries subscription and not call fetcher if data is not stale', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const appMock = getAppMock()
|
||||
const customClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 60,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
VueQueryPlugin.install(appMock, {
|
||||
queryClient: customClient,
|
||||
clientPersister: (client) => [
|
||||
vi.fn(),
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
client.setQueryData(key1, () => ({
|
||||
foo1: 'bar1',
|
||||
}))
|
||||
client.setQueryData(key2, () => ({
|
||||
foo2: 'bar2',
|
||||
}))
|
||||
resolve()
|
||||
}, 0)
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const fnSpy = vi.fn()
|
||||
|
||||
const query = useQuery(
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: fnSpy,
|
||||
},
|
||||
customClient,
|
||||
)
|
||||
|
||||
const queries = useQueries(
|
||||
{
|
||||
queries: [
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: fnSpy,
|
||||
},
|
||||
],
|
||||
},
|
||||
customClient,
|
||||
)
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(true)
|
||||
|
||||
expect(query.isFetching.value).toBe(false)
|
||||
expect(query.data.value).toStrictEqual(undefined)
|
||||
|
||||
expect(queries.value[0].isFetching).toBe(false)
|
||||
expect(queries.value[0].data).toStrictEqual(undefined)
|
||||
expect(fnSpy).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(customClient.isRestoring?.value).toBe(false)
|
||||
expect(query.data.value).toStrictEqual({ foo1: 'bar1' })
|
||||
expect(queries.value[0].data).toStrictEqual({ foo2: 'bar2' })
|
||||
expect(fnSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
258
packages/vue-query/src/devtools/devtools.ts
Normal file
258
packages/vue-query/src/devtools/devtools.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { setupDevtoolsPlugin } from '@vue/devtools-api'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import { onlineManager } from '@tanstack/query-core'
|
||||
import {
|
||||
getQueryStateLabel,
|
||||
getQueryStatusBg,
|
||||
getQueryStatusFg,
|
||||
sortFns,
|
||||
} from './utils'
|
||||
import type { CustomInspectorNode } from '@vue/devtools-api'
|
||||
import type { Query, QueryCacheNotifyEvent } from '@tanstack/query-core'
|
||||
import type { QueryClient } from '../queryClient'
|
||||
|
||||
const pluginId = 'vue-query'
|
||||
const pluginName = 'Vue Query'
|
||||
|
||||
export function setupDevtools(app: any, queryClient: QueryClient) {
|
||||
setupDevtoolsPlugin(
|
||||
{
|
||||
id: pluginId,
|
||||
label: pluginName,
|
||||
packageName: 'vue-query',
|
||||
homepage: 'https://tanstack.com/query/latest',
|
||||
logo: 'https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.svg',
|
||||
app,
|
||||
settings: {
|
||||
baseSort: {
|
||||
type: 'choice',
|
||||
component: 'button-group',
|
||||
label: 'Sort Cache Entries',
|
||||
options: [
|
||||
{
|
||||
label: 'ASC',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'DESC',
|
||||
value: -1,
|
||||
},
|
||||
],
|
||||
defaultValue: 1,
|
||||
},
|
||||
sortFn: {
|
||||
type: 'choice',
|
||||
label: 'Sort Function',
|
||||
options: Object.keys(sortFns).map((key) => ({
|
||||
label: key,
|
||||
value: key,
|
||||
})),
|
||||
defaultValue: Object.keys(sortFns)[0]!,
|
||||
},
|
||||
onlineMode: {
|
||||
type: 'choice',
|
||||
component: 'button-group',
|
||||
label: 'Online mode',
|
||||
options: [
|
||||
{
|
||||
label: 'Online',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'Offline',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
(api) => {
|
||||
const initialSettings = api.getSettings()
|
||||
onlineManager.setOnline(Boolean(initialSettings.onlineMode.valueOf()))
|
||||
|
||||
const queryCache = queryClient.getQueryCache()
|
||||
|
||||
api.addInspector({
|
||||
id: pluginId,
|
||||
label: pluginName,
|
||||
icon: 'api',
|
||||
nodeActions: [
|
||||
{
|
||||
icon: 'file_download',
|
||||
tooltip: 'Refetch',
|
||||
action: (queryHash: string) => {
|
||||
queryCache.get(queryHash)?.fetch()
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'alarm',
|
||||
tooltip: 'Invalidate',
|
||||
action: (queryHash: string) => {
|
||||
const query = queryCache.get(queryHash) as Query
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: query.queryKey,
|
||||
exact: true,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'settings_backup_restore',
|
||||
tooltip: 'Reset',
|
||||
action: (queryHash: string) => {
|
||||
queryCache.get(queryHash)?.reset()
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'delete',
|
||||
tooltip: 'Remove',
|
||||
action: (queryHash: string) => {
|
||||
const query = queryCache.get(queryHash) as Query
|
||||
queryCache.remove(query)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'hourglass_empty',
|
||||
tooltip: 'Force loading',
|
||||
action: (queryHash: string) => {
|
||||
const query = queryCache.get(queryHash) as Query
|
||||
|
||||
query.setState({
|
||||
data: undefined,
|
||||
status: 'pending',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'error_outline',
|
||||
tooltip: 'Force error',
|
||||
action: (queryHash: string) => {
|
||||
const query = queryCache.get(queryHash) as Query
|
||||
|
||||
query.setState({
|
||||
data: undefined,
|
||||
status: 'error',
|
||||
error: new Error('Unknown error from devtools'),
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
api.addTimelineLayer({
|
||||
id: pluginId,
|
||||
label: pluginName,
|
||||
color: 0xffd94c,
|
||||
})
|
||||
|
||||
queryCache.subscribe((event) => {
|
||||
api.sendInspectorTree(pluginId)
|
||||
api.sendInspectorState(pluginId)
|
||||
|
||||
const queryEvents: Array<QueryCacheNotifyEvent['type']> = [
|
||||
'added',
|
||||
'removed',
|
||||
'updated',
|
||||
]
|
||||
|
||||
if (queryEvents.includes(event.type)) {
|
||||
api.addTimelineEvent({
|
||||
layerId: pluginId,
|
||||
event: {
|
||||
title: event.type,
|
||||
subtitle: event.query.queryHash,
|
||||
time: api.now(),
|
||||
data: {
|
||||
queryHash: event.query.queryHash,
|
||||
...event,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
api.on.setPluginSettings((payload) => {
|
||||
if (payload.key === 'onlineMode') {
|
||||
onlineManager.setOnline(Boolean(payload.newValue))
|
||||
}
|
||||
})
|
||||
|
||||
api.on.getInspectorTree((payload) => {
|
||||
if (payload.inspectorId === pluginId) {
|
||||
const queries = queryCache.getAll()
|
||||
const settings = api.getSettings()
|
||||
|
||||
const filtered = payload.filter
|
||||
? queries.filter(
|
||||
(item) => rankItem(item.queryHash, payload.filter).passed,
|
||||
)
|
||||
: [...queries]
|
||||
|
||||
const sorted = filtered.sort(
|
||||
(a, b) => sortFns[settings.sortFn]!(a, b) * settings.baseSort,
|
||||
)
|
||||
|
||||
const nodes: Array<CustomInspectorNode> = sorted.map((query) => {
|
||||
const stateLabel = getQueryStateLabel(query)
|
||||
|
||||
return {
|
||||
id: query.queryHash,
|
||||
label: query.queryHash,
|
||||
tags: [
|
||||
{
|
||||
label: `${stateLabel} [${query.getObserversCount()}]`,
|
||||
textColor: getQueryStatusFg(query),
|
||||
backgroundColor: getQueryStatusBg(query),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
payload.rootNodes = nodes
|
||||
}
|
||||
})
|
||||
|
||||
api.on.getInspectorState((payload) => {
|
||||
if (payload.inspectorId === pluginId) {
|
||||
const query = queryCache.get(payload.nodeId)
|
||||
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
|
||||
payload.state = {
|
||||
' Query Details': [
|
||||
{
|
||||
key: 'Query key',
|
||||
value: query.queryHash,
|
||||
},
|
||||
{
|
||||
key: 'Query status',
|
||||
value: getQueryStateLabel(query),
|
||||
},
|
||||
{
|
||||
key: 'Observers',
|
||||
value: query.getObserversCount(),
|
||||
},
|
||||
{
|
||||
key: 'Last Updated',
|
||||
value: new Date(query.state.dataUpdatedAt).toLocaleTimeString(),
|
||||
},
|
||||
],
|
||||
'Data Explorer': [
|
||||
{
|
||||
key: 'Data',
|
||||
value: query.state.data,
|
||||
},
|
||||
],
|
||||
'Query Explorer': [
|
||||
{
|
||||
key: 'Query',
|
||||
value: query,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
95
packages/vue-query/src/devtools/utils.ts
Normal file
95
packages/vue-query/src/devtools/utils.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Query } from '@tanstack/query-core'
|
||||
|
||||
type SortFn = (a: Query, b: Query) => number
|
||||
|
||||
enum QueryState {
|
||||
Fetching = 0,
|
||||
Fresh,
|
||||
Stale,
|
||||
Inactive,
|
||||
Paused,
|
||||
}
|
||||
|
||||
export function getQueryState(query: Query): QueryState {
|
||||
if (query.state.fetchStatus === 'fetching') {
|
||||
return QueryState.Fetching
|
||||
}
|
||||
if (query.state.fetchStatus === 'paused') {
|
||||
return QueryState.Paused
|
||||
}
|
||||
if (!query.getObserversCount()) {
|
||||
return QueryState.Inactive
|
||||
}
|
||||
if (query.isStale()) {
|
||||
return QueryState.Stale
|
||||
}
|
||||
|
||||
return QueryState.Fresh
|
||||
}
|
||||
|
||||
export function getQueryStateLabel(query: Query): string {
|
||||
const queryState = getQueryState(query)
|
||||
|
||||
if (queryState === QueryState.Fetching) {
|
||||
return 'fetching'
|
||||
}
|
||||
if (queryState === QueryState.Paused) {
|
||||
return 'paused'
|
||||
}
|
||||
if (queryState === QueryState.Stale) {
|
||||
return 'stale'
|
||||
}
|
||||
if (queryState === QueryState.Inactive) {
|
||||
return 'inactive'
|
||||
}
|
||||
|
||||
return 'fresh'
|
||||
}
|
||||
|
||||
export function getQueryStatusFg(query: Query): number {
|
||||
const queryState = getQueryState(query)
|
||||
|
||||
if (queryState === QueryState.Stale) {
|
||||
return 0x000000
|
||||
}
|
||||
|
||||
return 0xffffff
|
||||
}
|
||||
|
||||
export function getQueryStatusBg(query: Query): number {
|
||||
const queryState = getQueryState(query)
|
||||
|
||||
if (queryState === QueryState.Fetching) {
|
||||
return 0x006bff
|
||||
}
|
||||
if (queryState === QueryState.Paused) {
|
||||
return 0x8c49eb
|
||||
}
|
||||
if (queryState === QueryState.Stale) {
|
||||
return 0xffb200
|
||||
}
|
||||
if (queryState === QueryState.Inactive) {
|
||||
return 0x3f4e60
|
||||
}
|
||||
|
||||
return 0x008327
|
||||
}
|
||||
|
||||
const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash)
|
||||
|
||||
const dateSort: SortFn = (a, b) =>
|
||||
a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1
|
||||
|
||||
const statusAndDateSort: SortFn = (a, b) => {
|
||||
if (getQueryState(a) === getQueryState(b)) {
|
||||
return dateSort(a, b)
|
||||
}
|
||||
|
||||
return getQueryState(a) > getQueryState(b) ? 1 : -1
|
||||
}
|
||||
|
||||
export const sortFns: Record<string, SortFn> = {
|
||||
'Status > Last Updated': statusAndDateSort,
|
||||
'Query Hash': queryHashSort,
|
||||
'Last Updated': dateSort,
|
||||
}
|
||||
49
packages/vue-query/src/index.ts
Normal file
49
packages/vue-query/src/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export * from '@tanstack/query-core'
|
||||
|
||||
export { useQueryClient } from './useQueryClient'
|
||||
export { VueQueryPlugin } from './vueQueryPlugin'
|
||||
|
||||
export { QueryClient } from './queryClient'
|
||||
export { QueryCache } from './queryCache'
|
||||
export { queryOptions } from './queryOptions'
|
||||
export type {
|
||||
QueryOptions,
|
||||
UseQueryOptions,
|
||||
UndefinedInitialQueryOptions,
|
||||
DefinedInitialQueryOptions,
|
||||
UndefinedInitialQueryOptionsWithDataTag,
|
||||
DefinedInitialQueryOptionsWithDataTag,
|
||||
} from './queryOptions'
|
||||
export { infiniteQueryOptions } from './infiniteQueryOptions'
|
||||
export type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
export { MutationCache } from './mutationCache'
|
||||
export { mutationOptions } from './mutationOptions'
|
||||
export { useQuery } from './useQuery'
|
||||
export { useQueries } from './useQueries'
|
||||
export { useInfiniteQuery } from './useInfiniteQuery'
|
||||
export { usePrefetchQuery } from './usePrefetchQuery'
|
||||
export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery'
|
||||
export { useMutation } from './useMutation'
|
||||
export { useIsFetching } from './useIsFetching'
|
||||
export { useIsMutating, useMutationState } from './useMutationState'
|
||||
export { VUE_QUERY_CLIENT } from './utils'
|
||||
|
||||
export type { UsePrefetchQueryOptions } from './usePrefetchQuery'
|
||||
export type { UsePrefetchInfiniteQueryOptions } from './usePrefetchInfiniteQuery'
|
||||
export type { UseQueryReturnType, UseQueryDefinedReturnType } from './useQuery'
|
||||
export type {
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryReturnType,
|
||||
} from './useInfiniteQuery'
|
||||
export type { UseMutationOptions, UseMutationReturnType } from './useMutation'
|
||||
export type { MutationOptions } from './types'
|
||||
export type { UseQueriesOptions, UseQueriesResults } from './useQueries'
|
||||
export type {
|
||||
MutationStateOptions,
|
||||
UseIsMutatingFilters,
|
||||
} from './useMutationState'
|
||||
export type { UseIsFetchingFilters } from './useIsFetching'
|
||||
export type { VueQueryPluginOptions } from './vueQueryPlugin'
|
||||
178
packages/vue-query/src/infiniteQueryOptions.ts
Normal file
178
packages/vue-query/src/infiniteQueryOptions.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
NonUndefinedGuard,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseInfiniteQueryOptions } from './useInfiniteQuery'
|
||||
|
||||
/**
|
||||
* The options accepted by the `infiniteQueryOptions` overload selected when no `initialData` is set — `data`
|
||||
* may be `undefined` while the query is `pending`.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs — defaults to `InfiniteData<TQueryFnData>`,
|
||||
* the shape of all fetched pages plus their page params.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export type UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
initialData?: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The options accepted by the `infiniteQueryOptions` overload selected when `initialData` is set — `data` is
|
||||
* never `undefined`.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs — defaults to `InfiniteData<TQueryFnData>`,
|
||||
* the shape of all fetched pages plus their page params.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export type DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
/**
|
||||
* If set, this value will be used as the initial data for the query cache (as long as the query hasn't been
|
||||
* created or cached yet). If set to a function, the function will be called **once** during the shared/root
|
||||
* query initialization, and be expected to synchronously return the initial data. Initial data is
|
||||
* considered stale by default unless a `staleTime` has been set. `initialData` **is persisted** to the
|
||||
* cache.
|
||||
*/
|
||||
initialData:
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
|
||||
}
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`.
|
||||
* These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`.
|
||||
* `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* @see {@link useInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link UndefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `useInfiniteQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const projectsOptions = infiniteQueryOptions({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
*
|
||||
* const { data, isError, error, fetchNextPage } = useInfiniteQuery(projectsOptions)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`.
|
||||
* These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`.
|
||||
* `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link useInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link DefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `useInfiniteQuery`, with `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const projectsOptions = infiniteQueryOptions({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* initialData: { pages: [], pageParams: [] },
|
||||
* })
|
||||
*
|
||||
* // `data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the
|
||||
* // list stays visible alongside the error.
|
||||
* const { data, isError, error } = useInfiniteQuery(projectsOptions)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
export function infiniteQueryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
30
packages/vue-query/src/mutationCache.ts
Normal file
30
packages/vue-query/src/mutationCache.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { MutationCache as MC } from '@tanstack/query-core'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type {
|
||||
DefaultError,
|
||||
Mutation,
|
||||
MutationFilters,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MaybeRefDeep } from './types'
|
||||
|
||||
/**
|
||||
* Vue-aware subclass of `@tanstack/query-core`'s `MutationCache`. `find`/`findAll` also accept a
|
||||
* {@link MaybeRefDeep} filters object, so `ref`s can be passed directly without unwrapping. Access it via
|
||||
* `queryClient.getMutationCache()` — `QueryClient` constructs one of these by default.
|
||||
*/
|
||||
export class MutationCache extends MC {
|
||||
find<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = any,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
filters: MaybeRefDeep<MutationFilters>,
|
||||
): Mutation<TData, TError, TVariables, TOnMutateResult> | undefined {
|
||||
return super.find(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
findAll(filters: MaybeRefDeep<MutationFilters> = {}): Array<Mutation> {
|
||||
return super.findAll(cloneDeepUnref(filters))
|
||||
}
|
||||
}
|
||||
185
packages/vue-query/src/mutationOptions.ts
Normal file
185
packages/vue-query/src/mutationOptions.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { DefaultError, WithRequired } from '@tanstack/query-core'
|
||||
import type { MutationOptions } from './types'
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. A
|
||||
* `mutationKey` is required on this overload so the mutation can be looked up later, e.g. with
|
||||
* `useMutationState`.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - The mutation options to use, identical to what you'd pass to `useMutation`, with a
|
||||
* required `mutationKey`.
|
||||
* @returns The same options object, unchanged.
|
||||
*
|
||||
* @example
|
||||
* Looking the mutation up elsewhere via its `mutationKey`, e.g. for a global "saving…" indicator:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { mutationOptions, useMutationState } from '@tanstack/vue-query'
|
||||
* import { computed } from 'vue'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationKey: ['posts', 'create'],
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* // Call `useMutationState` once — it manages its own subscription — then derive from its result.
|
||||
* const creatingPosts = useMutationState({
|
||||
* filters: { mutationKey: createPostOptions.mutationKey, status: 'pending' },
|
||||
* })
|
||||
* const isCreatingPost = computed(() => creatingPosts.value.length > 0)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
|
||||
/**
|
||||
* Same as the plain-object overload with a required `mutationKey`, but for options that close over reactive
|
||||
* state (`ref`s read inside the function body). Wrap them in a getter so `useMutation` and the other consumers
|
||||
* always read the current values instead of the ones captured when the options were created.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - A function returning the mutation options to use, with a required `mutationKey`,
|
||||
* re-evaluated on demand.
|
||||
* @returns A function that returns the same options object, unchanged.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { mutationOptions, useMutation } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ userId: number }>()
|
||||
*
|
||||
* const createPostOptions = mutationOptions(() => ({
|
||||
* mutationKey: ['posts', 'create'],
|
||||
* // Reacts to changes on `props.userId`, unlike the plain-object overload.
|
||||
* mutationFn: (title: string) => createPost({ title, userId: props.userId }),
|
||||
* }))
|
||||
*
|
||||
* const mutation = useMutation(createPostOptions)
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="mutation.mutate('Hello')">Create</button>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: () => WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): () => WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. No
|
||||
* `mutationKey` is required on this overload — use this when you don't need to target the mutation via a
|
||||
* `mutationKey` filter later (e.g. with `useMutationState`); it can still be observed through other filters,
|
||||
* such as `status`.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - The mutation options to use, identical to what you'd pass to `useMutation`, without a
|
||||
* `mutationKey`.
|
||||
* @returns The same options object, unchanged.
|
||||
* @remarks See the other overload's example for looking a mutation up via `useMutationState`.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { mutationOptions, useMutation } from '@tanstack/vue-query'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* const mutation = useMutation(createPostOptions)
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="mutation.mutate({ title: 'Hello' })">Create</button>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
|
||||
/**
|
||||
* Same as the plain-object overload without a `mutationKey`, but for options that close over reactive state
|
||||
* (`ref`s read inside the function body). Wrap them in a getter so `useMutation` and the other consumers
|
||||
* always read the current values instead of the ones captured when the options were created.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - A function returning the mutation options to use, without a `mutationKey`, re-evaluated on
|
||||
* demand.
|
||||
* @returns A function that returns the same options object, unchanged.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { mutationOptions, useMutation } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ userId: number }>()
|
||||
*
|
||||
* const createPostOptions = mutationOptions(() => ({
|
||||
* // Reacts to changes on `props.userId`, unlike the plain-object overload.
|
||||
* mutationFn: (title: string) => createPost({ title, userId: props.userId }),
|
||||
* }))
|
||||
*
|
||||
* const mutation = useMutation(createPostOptions)
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="mutation.mutate('Hello')">Create</button>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: () => Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): () => Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
export function mutationOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
26
packages/vue-query/src/queryCache.ts
Normal file
26
packages/vue-query/src/queryCache.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { QueryCache as QC } from '@tanstack/query-core'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type {
|
||||
DefaultError,
|
||||
Query,
|
||||
QueryFilters,
|
||||
WithRequired,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MaybeRefDeep } from './types'
|
||||
|
||||
/**
|
||||
* Vue-aware subclass of `@tanstack/query-core`'s `QueryCache`. `find`/`findAll` also accept a
|
||||
* {@link MaybeRefDeep} filters object, so `ref`s can be passed directly without unwrapping. Access it via
|
||||
* `queryClient.getQueryCache()` — `QueryClient` constructs one of these by default.
|
||||
*/
|
||||
export class QueryCache extends QC {
|
||||
find<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData>(
|
||||
filters: MaybeRefDeep<WithRequired<QueryFilters, 'queryKey'>>,
|
||||
): Query<TQueryFnData, TError, TData> | undefined {
|
||||
return super.find(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
findAll(filters: MaybeRefDeep<QueryFilters> = {}): Array<Query> {
|
||||
return super.findAll(cloneDeepUnref(filters))
|
||||
}
|
||||
}
|
||||
627
packages/vue-query/src/queryClient.ts
Normal file
627
packages/vue-query/src/queryClient.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
import { nextTick, ref } from 'vue-demi'
|
||||
import { QueryClient as QC } from '@tanstack/query-core'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import { QueryCache } from './queryCache'
|
||||
import { MutationCache } from './mutationCache'
|
||||
import type { UseQueryOptions } from './queryOptions'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type { MaybeRefDeep, NoUnknown, QueryClientConfig } from './types'
|
||||
import type {
|
||||
CancelOptions,
|
||||
DefaultError,
|
||||
DefaultOptions,
|
||||
EnsureQueryDataOptions,
|
||||
FetchInfiniteQueryOptions,
|
||||
FetchQueryOptions,
|
||||
InferDataFromTag,
|
||||
InfiniteData,
|
||||
InfiniteQueryExecuteOptions,
|
||||
InvalidateOptions,
|
||||
InvalidateQueryFilters,
|
||||
MutationFilters,
|
||||
MutationKey,
|
||||
MutationObserverOptions,
|
||||
OmitKeyof,
|
||||
QueryExecuteOptions,
|
||||
QueryFilters,
|
||||
QueryKey,
|
||||
QueryObserverOptions,
|
||||
QueryState,
|
||||
RefetchOptions,
|
||||
RefetchQueryFilters,
|
||||
ResetOptions,
|
||||
SetDataOptions,
|
||||
Updater,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* Vue-aware subclass of `@tanstack/query-core`'s `QueryClient`. Methods that accept `options` (such as
|
||||
* `CancelOptions` or `InvalidateOptions`) or filters (such as the `QueryFilters` accepted by
|
||||
* `invalidateQueries`) also accept a {@link MaybeRefDeep} version of it, so you can pass `ref`s directly
|
||||
* without unwrapping them yourself — e.g. `queryClient.invalidateQueries({ queryKey: ['post', myRef] })`.
|
||||
* Install one on your app with `VueQueryPlugin`, or retrieve it with `useQueryClient`.
|
||||
*/
|
||||
export class QueryClient extends QC {
|
||||
constructor(config: QueryClientConfig = {}) {
|
||||
const vueQueryConfig = {
|
||||
defaultOptions: config.defaultOptions,
|
||||
queryCache: config.queryCache || new QueryCache(),
|
||||
mutationCache: config.mutationCache || new MutationCache(),
|
||||
}
|
||||
super(vueQueryConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` while a `clientPersister` passed to `VueQueryPlugin` is restoring the cache. Queries don't fetch
|
||||
* while this is `true`. Defaults to `false` if no persister is configured.
|
||||
*/
|
||||
isRestoring?: Ref<boolean> = ref(false)
|
||||
|
||||
isFetching(filters: MaybeRefDeep<QueryFilters> = {}): number {
|
||||
return super.isFetching(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
isMutating(filters: MaybeRefDeep<MutationFilters> = {}): number {
|
||||
return super.isMutating(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
getQueryData<TData = unknown, TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
queryKey: TTaggedQueryKey,
|
||||
): InferDataFromTag<TData, TTaggedQueryKey> | undefined
|
||||
getQueryData<TData = unknown>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
): TData | undefined
|
||||
getQueryData<TData = unknown>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
): TData | undefined {
|
||||
return super.getQueryData(cloneDeepUnref(queryKey))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use queryClient.query({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version.
|
||||
*/
|
||||
ensureQueryData<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): Promise<TData>
|
||||
ensureQueryData<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): Promise<TData>
|
||||
ensureQueryData<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): Promise<TData> {
|
||||
// grandfathered deprecated wrapper implementation
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return super.ensureQueryData(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
getQueriesData<TData = unknown>(
|
||||
filters: MaybeRefDeep<QueryFilters>,
|
||||
): Array<[QueryKey, TData | undefined]> {
|
||||
return super.getQueriesData(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
setQueryData<
|
||||
TQueryFnData = unknown,
|
||||
TTaggedQueryKey extends QueryKey = QueryKey,
|
||||
TInferredQueryFnData = InferDataFromTag<TQueryFnData, TTaggedQueryKey>,
|
||||
>(
|
||||
queryKey: TTaggedQueryKey,
|
||||
updater: Updater<
|
||||
NoInfer<TInferredQueryFnData> | undefined,
|
||||
NoInfer<TInferredQueryFnData> | undefined
|
||||
>,
|
||||
options?: MaybeRefDeep<SetDataOptions>,
|
||||
): NoInfer<TInferredQueryFnData> | undefined
|
||||
setQueryData<TQueryFnData, TData = NoUnknown<TQueryFnData>>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
updater: Updater<NoInfer<TData> | undefined, NoInfer<TData> | undefined>,
|
||||
options?: MaybeRefDeep<SetDataOptions>,
|
||||
): NoInfer<TData> | undefined
|
||||
setQueryData<TData>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
updater: Updater<TData | undefined, TData | undefined>,
|
||||
options: MaybeRefDeep<SetDataOptions> = {},
|
||||
): NoInfer<TData> | undefined {
|
||||
return super.setQueryData<TData>(
|
||||
cloneDeepUnref(queryKey),
|
||||
updater,
|
||||
cloneDeepUnref(options),
|
||||
)
|
||||
}
|
||||
|
||||
setQueriesData<TData>(
|
||||
filters: MaybeRefDeep<QueryFilters>,
|
||||
updater: Updater<TData | undefined, TData | undefined>,
|
||||
options: MaybeRefDeep<SetDataOptions> = {},
|
||||
): Array<[QueryKey, TData | undefined]> {
|
||||
return super.setQueriesData(
|
||||
cloneDeepUnref(filters),
|
||||
updater,
|
||||
cloneDeepUnref(options),
|
||||
)
|
||||
}
|
||||
|
||||
getQueryState<TData = unknown, TError = DefaultError>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
): QueryState<TData, TError> | undefined {
|
||||
return super.getQueryState(cloneDeepUnref(queryKey))
|
||||
}
|
||||
|
||||
removeQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?: QueryFilters<TTaggedQueryKey>,
|
||||
): void
|
||||
removeQueries(filters: MaybeRefDeep<QueryFilters> = {}): void {
|
||||
return super.removeQueries(cloneDeepUnref(filters))
|
||||
}
|
||||
|
||||
resetQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?: QueryFilters<TTaggedQueryKey>,
|
||||
options?: MaybeRefDeep<ResetOptions>,
|
||||
): Promise<void>
|
||||
resetQueries(
|
||||
filters: MaybeRefDeep<QueryFilters> = {},
|
||||
options: MaybeRefDeep<ResetOptions> = {},
|
||||
): Promise<void> {
|
||||
return super.resetQueries(cloneDeepUnref(filters), cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
cancelQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?: QueryFilters<TTaggedQueryKey>,
|
||||
options?: MaybeRefDeep<CancelOptions>,
|
||||
): Promise<void>
|
||||
cancelQueries(
|
||||
filters: MaybeRefDeep<QueryFilters> = {},
|
||||
options: MaybeRefDeep<CancelOptions> = {},
|
||||
): Promise<void> {
|
||||
return super.cancelQueries(cloneDeepUnref(filters), cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
invalidateQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?:
|
||||
| InvalidateQueryFilters<TTaggedQueryKey>
|
||||
| (() => InvalidateQueryFilters<TTaggedQueryKey>),
|
||||
options?: MaybeRefDeep<InvalidateOptions>,
|
||||
): Promise<void>
|
||||
invalidateQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters:
|
||||
| MaybeRefDeep<InvalidateQueryFilters<TTaggedQueryKey>>
|
||||
| (() => InvalidateQueryFilters<TTaggedQueryKey>) = {},
|
||||
options: MaybeRefDeep<InvalidateOptions> | (() => InvalidateOptions) = {},
|
||||
): Promise<void> {
|
||||
const filtersCloned = cloneDeepUnref(
|
||||
filters as MaybeRefDeep<InvalidateQueryFilters<TTaggedQueryKey>>,
|
||||
)
|
||||
const optionsCloned = cloneDeepUnref(
|
||||
options as MaybeRefDeep<InvalidateOptions>,
|
||||
)
|
||||
|
||||
super.invalidateQueries(
|
||||
{ ...filtersCloned, refetchType: 'none' },
|
||||
optionsCloned,
|
||||
)
|
||||
|
||||
if (filtersCloned.refetchType === 'none') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const refetchFilters: RefetchQueryFilters<TTaggedQueryKey> = {
|
||||
...filtersCloned,
|
||||
type: filtersCloned.refetchType ?? filtersCloned.type ?? 'active',
|
||||
}
|
||||
|
||||
// (dosipiuk): We need to delay `refetchQueries` execution to next macro task for all reactive values to be updated.
|
||||
// This ensures that `context` in `queryFn` while `invalidating` along reactive variable change has correct
|
||||
return nextTick().then(() => {
|
||||
return super.refetchQueries(refetchFilters, optionsCloned)
|
||||
})
|
||||
}
|
||||
|
||||
refetchQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?: RefetchQueryFilters<TTaggedQueryKey>,
|
||||
options?: MaybeRefDeep<RefetchOptions>,
|
||||
): Promise<void>
|
||||
refetchQueries(
|
||||
filters: MaybeRefDeep<RefetchQueryFilters> = {},
|
||||
options: MaybeRefDeep<RefetchOptions> = {},
|
||||
): Promise<void> {
|
||||
return super.refetchQueries(
|
||||
cloneDeepUnref(filters),
|
||||
cloneDeepUnref(options),
|
||||
)
|
||||
}
|
||||
|
||||
// These one-shot imperative methods do not resolve top-level option getters.
|
||||
// Resolve getters explicitly before calling, e.g. queryClient.query(options()).
|
||||
query<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options: QueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): Promise<TData>
|
||||
query<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
QueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<TData>
|
||||
query<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
QueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<TData> {
|
||||
return super.query(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use queryClient.query(options) instead. This method will be removed in the next major version.
|
||||
*/
|
||||
fetchQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options: FetchQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): Promise<TData>
|
||||
fetchQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options:
|
||||
| MaybeRefDeep<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
|
||||
>
|
||||
| (() => FetchQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>),
|
||||
): Promise<TData>
|
||||
fetchQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
|
||||
>,
|
||||
): Promise<TData> {
|
||||
// grandfathered deprecated wrapper implementation
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return super.fetchQuery(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use queryClient.query(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version.
|
||||
*/
|
||||
prefetchQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): Promise<void>
|
||||
prefetchQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): Promise<void>
|
||||
prefetchQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): Promise<void> {
|
||||
// grandfathered deprecated wrapper implementation
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return super.prefetchQuery(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
// These one-shot imperative methods do not resolve top-level option getters.
|
||||
// Resolve getters explicitly before calling, e.g. queryClient.infiniteQuery(options()).
|
||||
infiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: InfiniteQueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): Promise<
|
||||
Array<TData> extends Array<InfiniteData<TQueryFnData>>
|
||||
? InfiniteData<TQueryFnData, TPageParam>
|
||||
: TData
|
||||
>
|
||||
infiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
InfiniteQueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<
|
||||
Array<TData> extends Array<InfiniteData<TQueryFnData>>
|
||||
? InfiniteData<TQueryFnData, TPageParam>
|
||||
: TData
|
||||
>
|
||||
infiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
InfiniteQueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<
|
||||
Array<TData> extends Array<InfiniteData<TQueryFnData>>
|
||||
? InfiniteData<TQueryFnData, TPageParam>
|
||||
: TData
|
||||
> {
|
||||
return super.infiniteQuery(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use queryClient.infiniteQuery(options) instead. This method will be removed in the next major version.
|
||||
*/
|
||||
fetchInfiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): Promise<InfiniteData<TData, TPageParam>>
|
||||
fetchInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<InfiniteData<TData, TPageParam>>
|
||||
fetchInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<InfiniteData<TData, TPageParam>> {
|
||||
// grandfathered deprecated wrapper implementation
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return super.fetchInfiniteQuery(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use void queryClient.infiniteQuery(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version.
|
||||
*/
|
||||
prefetchInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): Promise<void>
|
||||
prefetchInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<void>
|
||||
prefetchInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefDeep<
|
||||
FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): Promise<void> {
|
||||
// grandfathered deprecated wrapper implementation
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return super.prefetchInfiniteQuery(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
setDefaultOptions(options: MaybeRefDeep<DefaultOptions>): void {
|
||||
super.setDefaultOptions(cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
setQueryDefaults<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
>(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
options: MaybeRefDeep<
|
||||
Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryData>, 'queryKey'>
|
||||
>,
|
||||
): void {
|
||||
super.setQueryDefaults(cloneDeepUnref(queryKey), cloneDeepUnref(options))
|
||||
}
|
||||
|
||||
getQueryDefaults(
|
||||
queryKey: MaybeRefDeep<QueryKey>,
|
||||
): OmitKeyof<QueryObserverOptions<any, any, any, any, any>, 'queryKey'> {
|
||||
return super.getQueryDefaults(cloneDeepUnref(queryKey))
|
||||
}
|
||||
|
||||
setMutationDefaults<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
mutationKey: MaybeRefDeep<MutationKey>,
|
||||
options: MaybeRefDeep<
|
||||
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>
|
||||
>,
|
||||
): void {
|
||||
super.setMutationDefaults(
|
||||
cloneDeepUnref(mutationKey),
|
||||
cloneDeepUnref(options),
|
||||
)
|
||||
}
|
||||
|
||||
getMutationDefaults(
|
||||
mutationKey: MaybeRefDeep<MutationKey>,
|
||||
): MutationObserverOptions<any, any, any, any> {
|
||||
return super.getMutationDefaults(cloneDeepUnref(mutationKey))
|
||||
}
|
||||
}
|
||||
413
packages/vue-query/src/queryOptions.ts
Normal file
413
packages/vue-query/src/queryOptions.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import type {
|
||||
DeepUnwrapRef,
|
||||
MaybeRef,
|
||||
MaybeRefDeep,
|
||||
MaybeRefOrGetter,
|
||||
ShallowOption,
|
||||
} from './types'
|
||||
import type {
|
||||
DefaultError,
|
||||
InitialDataFunction,
|
||||
NonUndefinedGuard,
|
||||
QueryBooleanOption,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
QueryObserverOptions,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* The plain, unwrapped options that `queryOptions` hands back, and what `useQuery`, `useQueries`, and the
|
||||
* `queryClient` methods see once `ref`s have been resolved. To pass options in, use
|
||||
* {@link UseQueryOptions}, which accepts the same options as `ref`s and `computed`s too.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryData - The type of data stored in the cache, before `select` runs. Defaults to
|
||||
* `TQueryFnData` and can be configured independently of it.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type QueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = {
|
||||
[Property in keyof QueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>]: Property extends 'enabled'
|
||||
?
|
||||
| MaybeRefOrGetter<boolean | undefined>
|
||||
| (() => QueryBooleanOption<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TQueryData,
|
||||
DeepUnwrapRef<TQueryKey>
|
||||
>)
|
||||
: Property extends 'queryKey'
|
||||
? MaybeRefOrGetter<TQueryKey>
|
||||
: QueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
DeepUnwrapRef<TQueryKey>
|
||||
>[Property]
|
||||
} & ShallowOption
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForUseQuery = symbol
|
||||
|
||||
/**
|
||||
* The options accepted by `queryOptions`, `useQuery`, and the other query hooks. `enabled` tracks reactive
|
||||
* dependencies automatically as a `ref`, a plain value, or a reactive getter (`() => ...`). `queryKey` reacts
|
||||
* through a `ref` or a reactive getter for the array itself, or `ref`s and reactive getters as individual
|
||||
* entries. Other options are read once when passed as a plain value, and stay reactive when passed as a `ref`
|
||||
* or a `computed`. Only `enabled` and `queryKey` read a function as a getter, so `queryFn` reacts through a
|
||||
* `computed`: a function there is the query function itself.
|
||||
*
|
||||
* If you instead pass a getter for the whole options object (`useQuery(() => ({ ... }))`), every option
|
||||
* inside it — including `staleTime`, `retry`, and `select` — is re-evaluated whenever the getter's own
|
||||
* reactive dependencies change, since the entire object is recomputed.
|
||||
*
|
||||
* `select` only re-runs when `data` changes, or when the `select` function's own reference changes. Since a
|
||||
* Vue `setup()` function runs only once per component instance, an inline `select` function passed directly
|
||||
* to `queryOptions`/`useQuery` already has a stable reference across reactive updates. An inline `select`
|
||||
* created inside a whole-options getter is recreated — and so can change reference — every time that getter
|
||||
* re-evaluates.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryData - The type of data stored in the cache, before `select` runs. Defaults to
|
||||
* `TQueryFnData` and can be configured independently of it.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type UseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = MaybeRef<
|
||||
{
|
||||
[Property in keyof QueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>]: Property extends 'enabled' | 'queryKey'
|
||||
? QueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>[Property]
|
||||
: Property extends 'queryFn'
|
||||
? MaybeRefDeep<
|
||||
| QueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>[Property]
|
||||
| SkipTokenForUseQuery
|
||||
>
|
||||
: MaybeRefDeep<
|
||||
QueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>[Property]
|
||||
>
|
||||
} & ShallowOption
|
||||
>
|
||||
|
||||
type WithUndefinedInitialData<TQueryFnData> = {
|
||||
/**
|
||||
* If set, this value will be used as the initial data for the query cache (as long as the query hasn't been
|
||||
* created or cached yet). If set to a function, the function will be called **once** during the shared/root
|
||||
* query initialization, and be expected to synchronously return the initial data. Initial data is
|
||||
* considered stale by default unless a `staleTime` has been set. `initialData` **is persisted** to the
|
||||
* cache. Unlike `queryKey`/`enabled`, this is not reactive — it isn't re-evaluated on `ref` changes.
|
||||
*/
|
||||
initialData?:
|
||||
| undefined
|
||||
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
|
||||
| NonUndefinedGuard<TQueryFnData>
|
||||
}
|
||||
|
||||
type WithDefinedInitialData<TQueryFnData> = {
|
||||
/**
|
||||
* If set, this value will be used as the initial data for the query cache (as long as the query hasn't been
|
||||
* created or cached yet). If set to a function, the function will be called **once** during the shared/root
|
||||
* query initialization, and be expected to synchronously return the initial data. Initial data is
|
||||
* considered stale by default unless a `staleTime` has been set. `initialData` **is persisted** to the
|
||||
* cache. Unlike `queryKey`/`enabled`, this is not reactive — it isn't re-evaluated on `ref` changes.
|
||||
*/
|
||||
initialData:
|
||||
| NonUndefinedGuard<TQueryFnData>
|
||||
| (() => NonUndefinedGuard<TQueryFnData>)
|
||||
}
|
||||
|
||||
/**
|
||||
* The options accepted by the `queryOptions` overload selected when no `initialData` is set — `data` may be
|
||||
* `undefined` while the query is `pending`.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type UndefinedInitialQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
|
||||
WithUndefinedInitialData<TQueryFnData>
|
||||
|
||||
/**
|
||||
* The options accepted by the `queryOptions` overload selected when `initialData` is set — `data` is never
|
||||
* `undefined`.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type DefinedInitialQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
|
||||
WithDefinedInitialData<TQueryFnData>
|
||||
|
||||
export type UndefinedInitialQueryOptionsWithDataTag<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = QueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
|
||||
WithUndefinedInitialData<TQueryFnData> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
export type DefinedInitialQueryOptionsWithDataTag<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = QueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
|
||||
WithDefinedInitialData<TQueryFnData> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can
|
||||
* be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and
|
||||
* is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - The {@link DefinedInitialQueryOptions} to use — everything you can pass to `useQuery`, with
|
||||
* `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { queryOptions, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const postsOptions = queryOptions({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* })
|
||||
*
|
||||
* // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails,
|
||||
* // so the list stays visible alongside the error.
|
||||
* const { data, isError, error } = useQuery(postsOptions)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): DefinedInitialQueryOptionsWithDataTag<TQueryFnData, TError, TData, TQueryKey>
|
||||
|
||||
/**
|
||||
* Same as the plain-object overload, but for options that close over reactive state (`ref`s read inside the
|
||||
* function body). Wrap them in a getter so `queryClient` methods like `invalidateQueries`/`fetchQuery` always
|
||||
* read the current values instead of the ones captured when the options were created.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - A function returning the {@link DefinedInitialQueryOptions} to use, re-evaluated on demand.
|
||||
* @returns A function that returns the same options object, typed so that `queryKey` carries the inferred data
|
||||
* type.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { ref } from 'vue'
|
||||
* import { queryOptions, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const postId = ref(1)
|
||||
* const postOptions = queryOptions(() => ({
|
||||
* queryKey: ['post', postId.value],
|
||||
* queryFn: () => fetchPost(postId.value),
|
||||
* initialData: { id: postId.value, title: '' },
|
||||
* }))
|
||||
*
|
||||
* // Pass the getter itself, not `postOptions()`, so the query keeps reacting to `postId` changes.
|
||||
* const { data } = useQuery(postOptions)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: () => DefinedInitialQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>,
|
||||
): () => DefinedInitialQueryOptionsWithDataTag<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can
|
||||
* be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and
|
||||
* is the query key to generate options for.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - The {@link UndefinedInitialQueryOptions} to use — everything you can pass to `useQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory, so the same options object can be reused per `id`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { queryOptions, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* function postOptions(id: string) {
|
||||
* return queryOptions({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* const { data, isPending, isError, error } = useQuery(postOptions('1'))
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): UndefinedInitialQueryOptionsWithDataTag<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
|
||||
/**
|
||||
* Same as the plain-object overload, but for options that close over reactive state (`ref`s read inside the
|
||||
* function body). Wrap them in a getter so the `queryKey` — and anything else derived from a `ref` — reacts
|
||||
* to changes, and so `queryClient` methods like `invalidateQueries`/`fetchQuery` always read the current
|
||||
* values instead of the ones captured when the options were created.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - A function returning the {@link UndefinedInitialQueryOptions} to use, re-evaluated on
|
||||
* demand.
|
||||
* @returns A function that returns the same options object, typed so that `queryKey` carries the inferred
|
||||
* data type.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { ref } from 'vue'
|
||||
* import { queryOptions, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const postId = ref(1)
|
||||
* const postOptions = queryOptions(() => ({
|
||||
* queryKey: ['post', postId.value],
|
||||
* queryFn: () => fetchPost(postId.value),
|
||||
* }))
|
||||
*
|
||||
* // Pass the getter itself, not `postOptions()`, so the query keeps reacting to `postId` changes.
|
||||
* const { data } = useQuery(postOptions)
|
||||
* const queryClient = useQueryClient()
|
||||
* // Here, call `postOptions()` so `invalidateQueries` reads the current `queryKey` right away.
|
||||
* queryClient.invalidateQueries(postOptions())
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory that disables the query, type safe, until `postId` is set. The whole-options getter
|
||||
* re-evaluates `queryFn` on every change to `postId`. `queryFn` can also be a `computed`, but never a bare
|
||||
* getter, since a function there is the query function itself:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { queryOptions, skipToken, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* function postOptions(postId: number | undefined) {
|
||||
* return queryOptions(() => ({
|
||||
* queryKey: ['post', postId],
|
||||
* queryFn: postId != null ? () => fetchPost(postId) : skipToken,
|
||||
* }))
|
||||
* }
|
||||
*
|
||||
* const props = defineProps<{ postId: number | undefined }>()
|
||||
* const { data, isLoading, isError, error } = useQuery(postOptions(props.postId))
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: () => UndefinedInitialQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>,
|
||||
): () => UndefinedInitialQueryOptionsWithDataTag<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
|
||||
export function queryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
109
packages/vue-query/src/types.ts
Normal file
109
packages/vue-query/src/types.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
DehydrateOptions,
|
||||
HydrateOptions,
|
||||
MutationCache,
|
||||
MutationObserverOptions,
|
||||
OmitKeyof,
|
||||
QueryCache,
|
||||
QueryObserverOptions,
|
||||
} from '@tanstack/query-core'
|
||||
import type { ComputedRef, Ref, UnwrapRef } from 'vue-demi'
|
||||
|
||||
type Primitive = string | number | boolean | bigint | symbol | undefined | null
|
||||
type UnwrapLeaf =
|
||||
| Primitive
|
||||
| Function
|
||||
| Date
|
||||
| Error
|
||||
| RegExp
|
||||
| Map<any, any>
|
||||
| WeakMap<any, any>
|
||||
| Set<any>
|
||||
| WeakSet<any>
|
||||
|
||||
/** A plain value or a reactive getter (`() => T`) that returns one. */
|
||||
export type MaybeGetter<T> = T | (() => T)
|
||||
|
||||
/**
|
||||
* A plain value, a `Ref`, or a `ComputedRef`. Accepting this instead of a bare `T` lets a composable take
|
||||
* either a reactive or a static value for a given option — see the [Reactivity guide](../../reactivity.md).
|
||||
*/
|
||||
export type MaybeRef<T> = Ref<T> | ComputedRef<T> | T
|
||||
|
||||
/**
|
||||
* A {@link MaybeRef}, or a reactive getter (`() => T`). Reactive getters are a lighter-weight alternative to
|
||||
* `computed` for deriving a value from other reactive state — see the
|
||||
* [Reactivity guide](../../reactivity.md#using-derived-state-inside-queries).
|
||||
*/
|
||||
export type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T)
|
||||
|
||||
/**
|
||||
* Like {@link MaybeRef}, but applied recursively to every property of `T` — so each field of an options object
|
||||
* (for example, an entry inside a `queryKey` array) can independently be a plain value or a `ref`, not just the
|
||||
* object as a whole.
|
||||
*/
|
||||
export type MaybeRefDeep<T> = MaybeRef<
|
||||
T extends Function
|
||||
? T
|
||||
: T extends object
|
||||
? {
|
||||
[Property in keyof T]: MaybeRefDeep<T[Property]>
|
||||
}
|
||||
: T
|
||||
>
|
||||
|
||||
/** @internal Rejects `unknown`, collapsing it to `never` — used to keep generic inference from silently widening. */
|
||||
export type NoUnknown<T> = Equal<unknown, T> extends true ? never : T
|
||||
|
||||
/** @internal Type-level equality check between `TTargetA` and `TTargetB`. */
|
||||
export type Equal<TTargetA, TTargetB> =
|
||||
(<T>() => T extends TTargetA ? 1 : 2) extends <T>() => T extends TTargetB
|
||||
? 1
|
||||
: 2
|
||||
? true
|
||||
: false
|
||||
|
||||
/** The inverse of {@link MaybeRefDeep} — recursively unwraps any `Ref`s in `T` back to their plain value types. */
|
||||
export type DeepUnwrapRef<T> = T extends UnwrapLeaf
|
||||
? T
|
||||
: T extends Ref<infer U>
|
||||
? DeepUnwrapRef<U>
|
||||
: T extends {}
|
||||
? {
|
||||
[Property in keyof T]: DeepUnwrapRef<T[Property]>
|
||||
}
|
||||
: UnwrapRef<T>
|
||||
|
||||
export type ShallowOption = {
|
||||
/**
|
||||
* Return data in a shallow ref object (it is `false` by default). It can be set to `true` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive.
|
||||
*/
|
||||
shallow?: boolean
|
||||
}
|
||||
|
||||
export type MutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = OmitKeyof<
|
||||
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'_defaulted'
|
||||
> &
|
||||
ShallowOption
|
||||
|
||||
export interface DefaultOptions<TError = DefaultError> {
|
||||
queries?: OmitKeyof<QueryObserverOptions<unknown, TError>, 'queryKey'> &
|
||||
ShallowOption
|
||||
mutations?: MutationObserverOptions<unknown, TError, unknown, unknown> &
|
||||
ShallowOption
|
||||
hydrate?: HydrateOptions['defaultOptions']
|
||||
dehydrate?: DehydrateOptions
|
||||
}
|
||||
|
||||
export interface QueryClientConfig {
|
||||
queryCache?: QueryCache
|
||||
mutationCache?: MutationCache
|
||||
defaultOptions?: DefaultOptions
|
||||
}
|
||||
236
packages/vue-query/src/useBaseQuery.ts
Normal file
236
packages/vue-query/src/useBaseQuery.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
computed,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
reactive,
|
||||
readonly,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
import { shouldThrowError } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref, updateState } from './utils'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type {
|
||||
DefaultedQueryObserverOptions,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type { UseQueryOptions } from './queryOptions'
|
||||
import type { UseInfiniteQueryOptions } from './useInfiniteQuery'
|
||||
import type { MaybeRefOrGetter } from './types'
|
||||
|
||||
export type UseBaseQueryReturnType<
|
||||
TData,
|
||||
TError,
|
||||
TResult = QueryObserverResult<TData, TError>,
|
||||
> = {
|
||||
[K in keyof TResult]: K extends
|
||||
| 'fetchNextPage'
|
||||
| 'fetchPreviousPage'
|
||||
| 'refetch'
|
||||
? TResult[K]
|
||||
: Ref<Readonly<TResult>[K]>
|
||||
} & {
|
||||
suspense: () => Promise<TResult>
|
||||
}
|
||||
|
||||
type UseQueryOptionsGeneric<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> =
|
||||
| UseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
||||
| UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
|
||||
|
||||
/**
|
||||
* Base implementation shared by `useQuery` and `useInfiniteQuery`.
|
||||
*
|
||||
* @param Observer - The observer class from query-core (`QueryObserver` or `InfiniteQueryObserver`).
|
||||
* @param options - A `ref`, plain value, or reactive getter resolving to the query options.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
*/
|
||||
export function useBaseQuery<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
TPageParam,
|
||||
>(
|
||||
Observer: typeof QueryObserver,
|
||||
options: MaybeRefOrGetter<
|
||||
UseQueryOptionsGeneric<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseBaseQueryReturnType<TData, TError> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
const defaultedOptions = computed(() => {
|
||||
let resolvedOptions = options
|
||||
if (typeof resolvedOptions === 'function') {
|
||||
resolvedOptions = resolvedOptions()
|
||||
}
|
||||
const clonedOptions = cloneDeepUnref(resolvedOptions as any)
|
||||
|
||||
if (typeof clonedOptions.enabled === 'function') {
|
||||
clonedOptions.enabled = clonedOptions.enabled()
|
||||
}
|
||||
|
||||
const defaulted: DefaultedQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
> = client.defaultQueryOptions(clonedOptions)
|
||||
|
||||
defaulted._optimisticResults = client.isRestoring?.value
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
|
||||
return defaulted
|
||||
})
|
||||
|
||||
const observer = new Observer(client, defaultedOptions.value)
|
||||
// @ts-expect-error
|
||||
const state = defaultedOptions.value.shallow
|
||||
? shallowReactive(observer.getCurrentResult())
|
||||
: reactive(observer.getCurrentResult())
|
||||
|
||||
let unsubscribe = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
if (client.isRestoring) {
|
||||
watch(
|
||||
client.isRestoring,
|
||||
(isRestoring) => {
|
||||
if (!isRestoring) {
|
||||
unsubscribe()
|
||||
unsubscribe = observer.subscribe((result) => {
|
||||
updateState(state, result)
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
|
||||
const updater = () => {
|
||||
observer.setOptions(defaultedOptions.value)
|
||||
updateState(state, observer.getCurrentResult())
|
||||
}
|
||||
|
||||
watch(defaultedOptions, updater)
|
||||
|
||||
onScopeDispose(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
// fix #5910
|
||||
const refetch = (...args: Parameters<(typeof state)['refetch']>) => {
|
||||
updater()
|
||||
return state.refetch(...args)
|
||||
}
|
||||
|
||||
const suspense = () => {
|
||||
return new Promise<QueryObserverResult<TData, TError>>(
|
||||
(resolve, reject) => {
|
||||
let stopWatch = () => {
|
||||
// noop
|
||||
}
|
||||
const run = () => {
|
||||
if (defaultedOptions.value.enabled !== false) {
|
||||
// fix #6133
|
||||
observer.setOptions(defaultedOptions.value)
|
||||
const optimisticResult = observer.getOptimisticResult(
|
||||
defaultedOptions.value,
|
||||
)
|
||||
if (optimisticResult.isStale) {
|
||||
stopWatch()
|
||||
observer
|
||||
.fetchOptimistic(defaultedOptions.value)
|
||||
.then(resolve, (error: TError) => {
|
||||
if (
|
||||
shouldThrowError(defaultedOptions.value.throwOnError, [
|
||||
error,
|
||||
observer.getCurrentQuery(),
|
||||
])
|
||||
) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(observer.getCurrentResult())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
stopWatch()
|
||||
resolve(optimisticResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
stopWatch = watch(defaultedOptions, run)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Handle error boundary
|
||||
watch(
|
||||
() => state.error,
|
||||
(error) => {
|
||||
if (
|
||||
state.isError &&
|
||||
!state.isFetching &&
|
||||
shouldThrowError(defaultedOptions.value.throwOnError, [
|
||||
error as TError,
|
||||
observer.getCurrentQuery(),
|
||||
])
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// @ts-expect-error
|
||||
const readonlyState = defaultedOptions.value.shallow
|
||||
? shallowReadonly(state)
|
||||
: readonly(state)
|
||||
|
||||
const object: any = toRefs(readonlyState)
|
||||
for (const key in state) {
|
||||
if (typeof state[key as keyof typeof state] === 'function') {
|
||||
object[key] = state[key as keyof typeof state]
|
||||
}
|
||||
}
|
||||
|
||||
object.suspense = suspense
|
||||
object.refetch = refetch
|
||||
|
||||
return object as UseBaseQueryReturnType<TData, TError>
|
||||
}
|
||||
369
packages/vue-query/src/useInfiniteQuery.ts
Normal file
369
packages/vue-query/src/useInfiniteQuery.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
InfiniteQueryObserverOptions,
|
||||
InfiniteQueryObserverResult,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
import type { UseBaseQueryReturnType } from './useBaseQuery'
|
||||
|
||||
import type {
|
||||
DeepUnwrapRef,
|
||||
MaybeRef,
|
||||
MaybeRefDeep,
|
||||
MaybeRefOrGetter,
|
||||
ShallowOption,
|
||||
} from './types'
|
||||
import type { QueryClient } from './queryClient'
|
||||
|
||||
export type UseInfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = MaybeRef<
|
||||
{
|
||||
[Property in keyof InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>]: Property extends 'enabled'
|
||||
? MaybeRefOrGetter<
|
||||
InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
DeepUnwrapRef<TQueryKey>,
|
||||
TPageParam
|
||||
>[Property]
|
||||
>
|
||||
: MaybeRefDeep<
|
||||
InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
DeepUnwrapRef<TQueryKey>,
|
||||
TPageParam
|
||||
>[Property]
|
||||
>
|
||||
} & ShallowOption
|
||||
>
|
||||
|
||||
export type UseInfiniteQueryReturnType<TData, TError> = UseBaseQueryReturnType<
|
||||
TData,
|
||||
TError,
|
||||
InfiniteQueryObserverResult<TData, TError>
|
||||
>
|
||||
|
||||
/**
|
||||
* The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of
|
||||
* `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`.
|
||||
*
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` for the array itself, or `ref`s and reactive getters as
|
||||
* individual entries — the array itself can't be a bare getter.
|
||||
*
|
||||
* @remarks Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default
|
||||
* refetch behavior, resulting in outdated data. Make sure to call these functions only in response to user
|
||||
* actions, or add conditions like `hasNextPage && !isFetching`.
|
||||
* @param options - The {@link DefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `useInfiniteQuery`, with `initialData` set.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The same properties as `useQuery`, with the addition of `fetchNextPage`, `fetchPreviousPage`,
|
||||
* `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, and `isFetchingPreviousPage`. `data.value.pages` and
|
||||
* `data.value.pageParams` are also added, as long as a `select` doesn't change `TData` away from its default
|
||||
* `InfiniteData<TQueryFnData>` shape.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* // `data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the
|
||||
* // list stays visible alongside the error.
|
||||
* const { data, isError, error } = useInfiniteQuery({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* initialData: { pages: [], pageParams: [] },
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isError">Error: {{ error.message }}</span>
|
||||
* <ul>
|
||||
* <template v-for="page in data.pages" :key="page.nextId">
|
||||
* <li v-for="project in page.projects" :key="project.id">{{ project.name }}</li>
|
||||
* </template>
|
||||
* </ul>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseInfiniteQueryReturnType<TData, TError>
|
||||
|
||||
/**
|
||||
* The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of
|
||||
* `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`.
|
||||
*
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` for the array itself, or `ref`s and reactive getters as
|
||||
* individual entries — the array itself can't be a bare getter.
|
||||
*
|
||||
* @remarks Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default
|
||||
* refetch behavior, resulting in outdated data. Make sure to call these functions only in response to user
|
||||
* actions, or add conditions like `hasNextPage && !isFetching`.
|
||||
* @param options - The {@link UndefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `useInfiniteQuery`.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The same properties as `useQuery`, with the addition of `fetchNextPage`, `fetchPreviousPage`,
|
||||
* `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, and `isFetchingPreviousPage`. `data.value.pages` and
|
||||
* `data.value.pageParams` are also added, as long as a `select` doesn't change `TData` away from its default
|
||||
* `InfiniteData<TQueryFnData>` shape.
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page from a "Load More" button click:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const {
|
||||
* data,
|
||||
* isPending,
|
||||
* isError,
|
||||
* error,
|
||||
* fetchNextPage,
|
||||
* hasNextPage,
|
||||
* isFetching,
|
||||
* isFetchingNextPage,
|
||||
* } = useInfiniteQuery({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isPending">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <template v-else>
|
||||
* <ul>
|
||||
* <template v-for="page in data.pages" :key="page.nextId">
|
||||
* <li v-for="project in page.projects" :key="project.id">{{ project.name }}</li>
|
||||
* </template>
|
||||
* </ul>
|
||||
* <button @click="fetchNextPage()" :disabled="!hasNextPage || isFetching">
|
||||
* {{ isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load' }}
|
||||
* </button>
|
||||
* </template>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a
|
||||
* sentinel element after the list:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { onUnmounted, ref, watch } from 'vue'
|
||||
* import { useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const {
|
||||
* data,
|
||||
* isPending,
|
||||
* isError,
|
||||
* error,
|
||||
* fetchNextPage,
|
||||
* hasNextPage,
|
||||
* isFetching,
|
||||
* isFetchingNextPage,
|
||||
* } = useInfiniteQuery({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
*
|
||||
* const sentinel = ref<HTMLElement>()
|
||||
* let observer: IntersectionObserver | undefined
|
||||
*
|
||||
* watch(sentinel, (el) => {
|
||||
* observer?.disconnect()
|
||||
* if (el == null) return
|
||||
*
|
||||
* observer = new IntersectionObserver(([entry]) => {
|
||||
* if (entry?.isIntersecting && hasNextPage.value && !isFetching.value) {
|
||||
* fetchNextPage()
|
||||
* }
|
||||
* })
|
||||
* observer.observe(el)
|
||||
* })
|
||||
*
|
||||
* onUnmounted(() => observer?.disconnect())
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isPending">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <template v-else>
|
||||
* <ul>
|
||||
* <template v-for="page in data.pages" :key="page.nextId">
|
||||
* <li v-for="project in page.projects" :key="project.id">{{ project.name }}</li>
|
||||
* </template>
|
||||
* </ul>
|
||||
* <div ref="sentinel">{{ isFetchingNextPage ? 'Loading more...' : '' }}</div>
|
||||
* </template>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseInfiniteQueryReturnType<TData, TError>
|
||||
|
||||
/**
|
||||
* Fallback overload for options whose `initialData` presence isn't statically known — for example, a
|
||||
* `ref`/reactive object built up conditionally, rather than a plain object literal. Prefer one of the other
|
||||
* overloads when possible, since they infer whether `data` can be `undefined` from `initialData` directly.
|
||||
*
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` for the array itself, or `ref`s and reactive getters as
|
||||
* individual entries — the array itself can't be a bare getter.
|
||||
*
|
||||
* @param options - A `ref`, plain value, or reactive getter resolving to the {@link UseInfiniteQueryOptions} to
|
||||
* use.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The same properties as `useQuery`, with the addition of `fetchNextPage`, `fetchPreviousPage`,
|
||||
* `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, and `isFetchingPreviousPage`.
|
||||
*
|
||||
* @example
|
||||
* Passing a whole-options getter so `maxPages` reacts to a setting stored elsewhere, not just `queryKey`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ projectId: number; maxPages: number }>()
|
||||
*
|
||||
* const { data } = useInfiniteQuery(() => ({
|
||||
* queryKey: ['project', props.projectId, 'issues'],
|
||||
* queryFn: ({ pageParam }) => fetchIssues(props.projectId, pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* maxPages: props.maxPages,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <template v-for="page in data?.pages" :key="page.nextId">
|
||||
* <li v-for="issue in page.issues" :key="issue.id">{{ issue.title }}</li>
|
||||
* </template>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A query that's disabled, type-safe, until `postId` is set — pass `skipToken` as `queryFn` instead of
|
||||
* setting `enabled: false`. This requires a whole-options getter: `queryFn` isn't itself reactive, so the
|
||||
* getter is what re-evaluates it on every change to `props.postId`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { skipToken, useInfiniteQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ postId: string | undefined }>()
|
||||
*
|
||||
* // Use `isLoading`, not `isPending`, so the loading state doesn't show while the query is disabled.
|
||||
* const { data, isLoading, isError, error } = useInfiniteQuery(() => {
|
||||
* const postId = props.postId
|
||||
* return {
|
||||
* queryKey: ['post', postId, 'comments'],
|
||||
* queryFn:
|
||||
* postId != null
|
||||
* ? ({ pageParam }) => fetchComments(postId, pageParam)
|
||||
* : skipToken,
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* }
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="props.postId == null">Select a post</span>
|
||||
* <span v-else-if="isLoading">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <ul v-else>
|
||||
* <template v-for="page in data.pages" :key="page.nextId">
|
||||
* <li v-for="comment in page.comments" :key="comment.id">{{ comment.text }}</li>
|
||||
* </template>
|
||||
* </ul>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseInfiniteQueryReturnType<TData, TError>
|
||||
|
||||
export function useInfiniteQuery(
|
||||
options: MaybeRefOrGetter<UseInfiniteQueryOptions>,
|
||||
queryClient?: QueryClient,
|
||||
) {
|
||||
return useBaseQuery(
|
||||
InfiniteQueryObserver as typeof QueryObserver,
|
||||
options,
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
87
packages/vue-query/src/useIsFetching.ts
Normal file
87
packages/vue-query/src/useIsFetching.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { getCurrentScope, onScopeDispose, ref, watchEffect } from 'vue-demi'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type { QueryFilters } from '@tanstack/query-core'
|
||||
import type { MaybeRefDeep } from './types'
|
||||
import type { QueryClient } from './queryClient'
|
||||
|
||||
export type UseIsFetchingFilters =
|
||||
| MaybeRefDeep<QueryFilters>
|
||||
| (() => MaybeRefDeep<QueryFilters>)
|
||||
|
||||
/**
|
||||
* The `useIsFetching` composable returns a `ref` to the `number` of the queries that your application is
|
||||
* loading or fetching in the background (useful for app-wide loading indicators).
|
||||
*
|
||||
* `fetchingFilters` may be a plain object, `MaybeRefDeep`, or a reactive getter (`() => ({ ... })`) — pass a
|
||||
* getter if the filters themselves depend on other reactive state.
|
||||
*
|
||||
* @param fetchingFilters - The {@link QueryFilters} to narrow down the matched queries.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns A `ref` to the `number` of the queries that your application is currently loading or fetching in
|
||||
* the background.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useIsFetching } from '@tanstack/vue-query'
|
||||
*
|
||||
* // How many queries matching the posts prefix are fetching?
|
||||
* const isFetchingPosts = useIsFetching({ queryKey: ['posts'] })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isFetchingPosts">Refreshing posts...</span>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A global loading indicator for any query fetching in the background, not just the ones on screen:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useIsFetching } from '@tanstack/vue-query'
|
||||
*
|
||||
* const isFetching = useIsFetching()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <div v-if="isFetching">Queries are fetching in the background...</div>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useIsFetching(
|
||||
fetchingFilters: UseIsFetchingFilters = {},
|
||||
queryClient?: QueryClient,
|
||||
): Ref<number> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
const isFetching = ref()
|
||||
|
||||
const listener = () => {
|
||||
const resolvedFilters =
|
||||
typeof fetchingFilters === 'function'
|
||||
? fetchingFilters()
|
||||
: fetchingFilters
|
||||
isFetching.value = client.isFetching(cloneDeepUnref(resolvedFilters))
|
||||
}
|
||||
|
||||
const unsubscribe = client.getQueryCache().subscribe(listener)
|
||||
|
||||
watchEffect(listener)
|
||||
|
||||
onScopeDispose(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
return isFetching
|
||||
}
|
||||
306
packages/vue-query/src/useMutation.ts
Normal file
306
packages/vue-query/src/useMutation.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
computed,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
reactive,
|
||||
readonly,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
import { MutationObserver, shouldThrowError } from '@tanstack/query-core'
|
||||
import { cloneDeepUnref, updateState } from './utils'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import type { ToRefs } from 'vue-demi'
|
||||
import type {
|
||||
DefaultError,
|
||||
DistributiveOmit,
|
||||
MutateFunction,
|
||||
MutationObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MaybeRefDeep, MutationOptions } from './types'
|
||||
import type { QueryClient } from './queryClient'
|
||||
|
||||
type MutationResult<TData, TError, TVariables, TOnMutateResult> =
|
||||
DistributiveOmit<
|
||||
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutate' | 'reset'
|
||||
>
|
||||
|
||||
export type UseMutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> =
|
||||
| MaybeRefDeep<MutationOptions<TData, TError, TVariables, TOnMutateResult>>
|
||||
| (() => MaybeRefDeep<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>
|
||||
>)
|
||||
|
||||
type MutateSyncFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = (
|
||||
...options: Parameters<
|
||||
MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>
|
||||
) => void
|
||||
|
||||
export type UseMutationReturnType<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult,
|
||||
TResult = MutationResult<TData, TError, TVariables, TOnMutateResult>,
|
||||
> = ToRefs<Readonly<TResult>> & {
|
||||
mutate: MutateSyncFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
mutateAsync: MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
reset: MutationObserverResult<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
>['reset']
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
|
||||
* `useMutation` is the composable for that.
|
||||
*
|
||||
* `options` may be a plain object, a `ref`, or a reactive getter (`() => ({ ... })`) — pass a getter
|
||||
* if the options themselves depend on other reactive state.
|
||||
*
|
||||
* @see {@link mutationOptions} to share these options across multiple `useMutation` call sites, or to look
|
||||
* the mutation up elsewhere via its `mutationKey` (e.g. with `useMutationState`).
|
||||
* @param options - The {@link UseMutationOptions} to use — everything you can pass to `useMutation`.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns `mutate`/`mutateAsync` also accept per-call `onSuccess`/`onError`/`onSettled` callbacks as a second
|
||||
* argument, useful for triggering call-site side effects (e.g. navigation) without coupling them to the shared
|
||||
* mutation definition. Hook-level callbacks (passed to `options`) fire for every mutation; per-call
|
||||
* callbacks fire only for the latest call you've made.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* })
|
||||
*
|
||||
* function onAdd() {
|
||||
* addMutation.mutate('Item', {
|
||||
* onError: (error) => console.error('Failed to add item:', error),
|
||||
* })
|
||||
* }
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="onAdd">Add</button>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Rendering the mutation's own state, rather than just firing it off:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <div v-if="addMutation.isPending.value">Adding todo...</div>
|
||||
* <div v-else>
|
||||
* <div v-if="addMutation.isError.value">An error occurred: {{ addMutation.error.value.message }}</div>
|
||||
* <button @click="addMutation.mutate('Item')">Add</button>
|
||||
* </div>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Optimistic update via `onMutate`, rolling back on `onError`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation({
|
||||
* mutationFn: addTodo,
|
||||
* onMutate: async (newTodo: string) => {
|
||||
* await queryClient.cancelQueries({ queryKey: ['todos'] })
|
||||
* const previousTodos = queryClient.getQueryData<Array<string>>(['todos'])
|
||||
*
|
||||
* queryClient.setQueryData<Array<string>>(['todos'], (old) => [
|
||||
* ...(old ?? []),
|
||||
* newTodo,
|
||||
* ])
|
||||
*
|
||||
* // Passed to `onError` as `onMutateResult` if the mutation fails.
|
||||
* return { previousTodos }
|
||||
* },
|
||||
* onError: (_err, _newTodo, onMutateResult) => {
|
||||
* queryClient.setQueryData(['todos'], onMutateResult?.previousTodos)
|
||||
* },
|
||||
* onSettled: () => {
|
||||
* queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
* },
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="addMutation.mutate('Item')">Add</button>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Callbacks passed per call to `mutate` only fire for the last call — `mutateAsync` gives you a
|
||||
* promise per call instead, so you can wait for all of them when they succeed:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* })
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* try {
|
||||
* await Promise.all(todos.map((todo) => addMutation.mutateAsync(todo)))
|
||||
* } catch (error) {
|
||||
* console.error('Failed to add todos:', error)
|
||||
* }
|
||||
* }
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])">Add all</button>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* If some of the mutations above can fail independently of the others, and you want to know which ones
|
||||
* did — rather than losing that information the moment the first one rejects — swap `Promise.all` for
|
||||
* `Promise.allSettled`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* })
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* const addResults = await Promise.allSettled(
|
||||
* todos.map((todo) => addMutation.mutateAsync(todo)),
|
||||
* )
|
||||
*
|
||||
* addResults.forEach((addResult, index) => {
|
||||
* if (addResult.status === 'rejected') {
|
||||
* console.error(`Failed to add "${todos[index]}":`, addResult.reason)
|
||||
* }
|
||||
* })
|
||||
* }
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])">Add all</button>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useMutation<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationReturnType<TData, TError, TVariables, TOnMutateResult> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
const defaultedOptions = computed(() => {
|
||||
const resolvedOptions = typeof options === 'function' ? options() : options
|
||||
return client.defaultMutationOptions(cloneDeepUnref(resolvedOptions))
|
||||
})
|
||||
const observer = new MutationObserver(client, defaultedOptions.value)
|
||||
const state = defaultedOptions.value.shallow
|
||||
? shallowReactive(observer.getCurrentResult())
|
||||
: reactive(observer.getCurrentResult())
|
||||
|
||||
const unsubscribe = observer.subscribe((result) => {
|
||||
updateState(state, result)
|
||||
})
|
||||
|
||||
const mutate = (
|
||||
...args: Parameters<
|
||||
MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>
|
||||
) => {
|
||||
observer.mutate(args[0] as TVariables, args[1]).catch(() => {
|
||||
// This is intentional
|
||||
})
|
||||
}
|
||||
|
||||
watch(defaultedOptions, () => {
|
||||
observer.setOptions(defaultedOptions.value)
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
const readonlyState = defaultedOptions.value.shallow
|
||||
? shallowReadonly(state)
|
||||
: readonly(state)
|
||||
|
||||
const resultRefs = toRefs(readonlyState) as ToRefs<
|
||||
Readonly<MutationResult<TData, TError, TVariables, TOnMutateResult>>
|
||||
>
|
||||
|
||||
watch(
|
||||
() => state.error,
|
||||
(error) => {
|
||||
if (
|
||||
error &&
|
||||
shouldThrowError(defaultedOptions.value.throwOnError, [error as TError])
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...resultRefs,
|
||||
mutate,
|
||||
mutateAsync: state.mutate,
|
||||
reset: state.reset,
|
||||
}
|
||||
}
|
||||
228
packages/vue-query/src/useMutationState.ts
Normal file
228
packages/vue-query/src/useMutationState.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
computed,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationFilters,
|
||||
MutationState,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type { MaybeRefDeep } from './types'
|
||||
import type { MutationCache } from './mutationCache'
|
||||
|
||||
type VueMutationFilters = MaybeRefDeep<MutationFilters>
|
||||
|
||||
export type UseIsMutatingFilters =
|
||||
| VueMutationFilters
|
||||
| (() => VueMutationFilters)
|
||||
|
||||
/**
|
||||
* The `useIsMutating` composable returns a `ref` to the `number` of mutations that your application currently
|
||||
* has `pending` (useful for app-wide loading indicators).
|
||||
*
|
||||
* `filters` may be a plain object, `MaybeRefDeep`, or a reactive getter (`() => ({ ... })`) — pass a getter if
|
||||
* the filters themselves depend on other reactive state.
|
||||
*
|
||||
* @param filters - The {@link MutationFilters} to narrow down the matched mutations.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns A `ref` to the `number` of the mutations that your application currently has `pending`.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useIsMutating } from '@tanstack/vue-query'
|
||||
*
|
||||
* // How many mutations matching the posts prefix are in progress?
|
||||
* const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isMutatingPosts">Saving posts...</span>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useIsMutating(
|
||||
filters: UseIsMutatingFilters = {},
|
||||
queryClient?: QueryClient,
|
||||
): Ref<number> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
const mutationState = useMutationState(
|
||||
{
|
||||
filters: computed(() => ({
|
||||
...cloneDeepUnref(typeof filters === 'function' ? filters() : filters),
|
||||
status: 'pending' as const,
|
||||
})),
|
||||
},
|
||||
client,
|
||||
)
|
||||
const length = computed(() => mutationState.value.length)
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
type MutationTypeFromResult<TResult> = [TResult] extends [
|
||||
MutationState<
|
||||
infer TData,
|
||||
infer TError,
|
||||
infer TVariables,
|
||||
infer TOnMutateResult
|
||||
>,
|
||||
]
|
||||
? Mutation<TData, TError, TVariables, TOnMutateResult>
|
||||
: Mutation
|
||||
|
||||
export type MutationStateOptions<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
> = {
|
||||
filters?: VueMutationFilters
|
||||
select?: (mutation: TMutation) => TResult
|
||||
}
|
||||
|
||||
function getResult<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
mutationCache: MutationCache,
|
||||
options: MutationStateOptions<TResult, TMutation>,
|
||||
): Array<TResult> {
|
||||
return mutationCache
|
||||
.findAll(options.filters)
|
||||
.map(
|
||||
(mutation): TResult =>
|
||||
(options.select
|
||||
? options.select(mutation as TMutation)
|
||||
: mutation.state) as TResult,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* `useMutationState` is a composable that gives you access to all mutations in the `MutationCache`. You can
|
||||
* pass `filters` ({@link MutationFilters}) to narrow down your mutations, and `select` to transform the
|
||||
* mutation state.
|
||||
*
|
||||
* `options` may be a plain object or a reactive getter (`() => ({ ... })`) — pass a getter if the filters
|
||||
* themselves depend on other reactive state.
|
||||
*
|
||||
* @param options - The `filters` to narrow down matched mutations, and an optional `select` to transform the
|
||||
* mutation state.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns A `ref` to an Array of whatever `select` returns for each matching mutation.
|
||||
*
|
||||
* @example
|
||||
* Get all variables of all running mutations:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutationState } from '@tanstack/vue-query'
|
||||
*
|
||||
* const pendingVariables = useMutationState({
|
||||
* filters: { status: 'pending' },
|
||||
* select: (mutation) => mutation.state.variables,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>{{ pendingVariables.length }} posts saving...</template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Get all data for specific mutations via the `mutationKey`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useMutation, useMutationState } from '@tanstack/vue-query'
|
||||
*
|
||||
* const mutationKey = ['posts']
|
||||
*
|
||||
* // Some mutation that we want to get the state for
|
||||
* const mutation = useMutation({
|
||||
* mutationKey,
|
||||
* mutationFn: createPosts,
|
||||
* })
|
||||
*
|
||||
* const savedPosts = useMutationState({
|
||||
* // this mutation key needs to match the mutation key of the given mutation (see above)
|
||||
* filters: { mutationKey, status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <button @click="mutation.mutate(['New Post'])">
|
||||
* Create post ({{ savedPosts.length }} saved so far)
|
||||
* </button>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Access the latest successful mutation data via the `mutationKey`. Each invocation of `mutate` adds a new
|
||||
* entry to the mutation cache for `gcTime` milliseconds — with the `status: 'success'` filter below, check the
|
||||
* last item that `useMutationState` returns to get the latest successful invocation:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { computed } from 'vue'
|
||||
* import { useMutationState } from '@tanstack/vue-query'
|
||||
*
|
||||
* const savedPosts = useMutationState({
|
||||
* filters: { mutationKey: ['posts'], status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* })
|
||||
*
|
||||
* const latestSavedPost = computed(() => savedPosts.value[savedPosts.value.length - 1])
|
||||
* </script>
|
||||
*
|
||||
* <template>{{ latestSavedPost ? 'Saved' : 'Nothing saved yet' }}</template>
|
||||
* ```
|
||||
*/
|
||||
export function useMutationState<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
options:
|
||||
| MutationStateOptions<TResult, TMutation>
|
||||
| (() => MutationStateOptions<TResult, TMutation>) = {},
|
||||
queryClient?: QueryClient,
|
||||
): Readonly<Ref<Array<TResult>>> {
|
||||
const resolvedOptions = computed(() => {
|
||||
const newOptions = typeof options === 'function' ? options() : options
|
||||
return {
|
||||
filters: cloneDeepUnref(newOptions.filters),
|
||||
select: newOptions.select,
|
||||
}
|
||||
})
|
||||
const mutationCache = (queryClient || useQueryClient()).getMutationCache()
|
||||
const state = shallowRef(getResult(mutationCache, resolvedOptions.value))
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
state.value = getResult(mutationCache, resolvedOptions.value)
|
||||
})
|
||||
|
||||
watch(resolvedOptions, () => {
|
||||
state.value = getResult(mutationCache, resolvedOptions.value)
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
return shallowReadonly(state)
|
||||
}
|
||||
139
packages/vue-query/src/usePrefetchInfiniteQuery.ts
Normal file
139
packages/vue-query/src/usePrefetchInfiniteQuery.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { getCurrentScope, unref, watchEffect } from 'vue-demi'
|
||||
import { noop } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type {
|
||||
DefaultError,
|
||||
DistributiveOmit,
|
||||
InfiniteData,
|
||||
InfiniteQueryExecuteOptions,
|
||||
QueryKey,
|
||||
SkipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type { MaybeRefDeep, MaybeRefOrGetter } from './types'
|
||||
|
||||
export type UsePrefetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey extends QueryKey,
|
||||
TPageParam,
|
||||
> = DistributiveOmit<
|
||||
InfiniteQueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
'queryFn'
|
||||
> & {
|
||||
queryFn?: Exclude<
|
||||
InfiniteQueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>['queryFn'],
|
||||
SkipToken
|
||||
>
|
||||
}
|
||||
|
||||
function isGetter<T>(value: MaybeRefOrGetter<T>): value is () => T {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* `usePrefetchInfiniteQuery` does not return anything — it fires a prefetch as a reactive side effect, useful
|
||||
* for kicking off a fetch ahead of the component that will actually render the data with `useInfiniteQuery`.
|
||||
* You can pass everything to `usePrefetchInfiniteQuery` that you can pass to `queryClient.infiniteQuery`,
|
||||
* though `queryKey`, `initialPageParam`, and `getNextPageParam` are always required, and `queryFn` is required
|
||||
* unless a default query function has been defined.
|
||||
*
|
||||
* `getNextPageParam` receives both the last page of the infinite list of data and the full array of all pages,
|
||||
* as well as pageParam information, and should return a single variable that will be passed to your query
|
||||
* function as `context.pageParam`. Return `undefined` or `null` to indicate there is no next page available.
|
||||
*
|
||||
* The prefetch is skipped if the query already has any cached state — including a `pending`/`error` state left
|
||||
* over from a previous attempt — so it won't refetch data that's already there or already in flight. It
|
||||
* re-runs whenever a reactive dependency in `options` (built with `infiniteQueryOptions`, for example) changes.
|
||||
*
|
||||
* Fire this during render, before a suspense boundary that wraps a component using `useInfiniteQuery`'s
|
||||
* `suspense()` — see the {@link https://tanstack.com/query/latest/docs/framework/vue/guides/suspense | Suspense guide}.
|
||||
*
|
||||
* @param options - A `ref`, plain value, or reactive getter resolving to the
|
||||
* {@link UsePrefetchInfiniteQueryOptions} to use — everything you can pass to `queryClient.infiniteQuery`.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns `void` — nothing is returned.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { infiniteQueryOptions, usePrefetchInfiniteQuery } from '@tanstack/vue-query'
|
||||
* import Projects from './Projects.vue'
|
||||
*
|
||||
* const projectsOptions = infiniteQueryOptions({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
*
|
||||
* // Fire the prefetch as soon as this component runs, before `Projects` mounts and calls `useInfiniteQuery`.
|
||||
* usePrefetchInfiniteQuery(projectsOptions)
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <Projects />
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function usePrefetchInfiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
MaybeRefDeep<
|
||||
UsePrefetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): void {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
watchEffect(() => {
|
||||
const resolvedOptions = isGetter(options) ? options() : unref(options)
|
||||
|
||||
const clonedOptions: UsePrefetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> = cloneDeepUnref(resolvedOptions)
|
||||
|
||||
if (!client.getQueryState(clonedOptions.queryKey)) {
|
||||
void client.infiniteQuery(clonedOptions).then(noop).catch(noop)
|
||||
}
|
||||
})
|
||||
}
|
||||
130
packages/vue-query/src/usePrefetchQuery.ts
Normal file
130
packages/vue-query/src/usePrefetchQuery.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { getCurrentScope, unref, watchEffect } from 'vue-demi'
|
||||
import { noop } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type {
|
||||
DefaultError,
|
||||
OmitKeyof,
|
||||
QueryExecuteOptions,
|
||||
QueryKey,
|
||||
SkipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type { MaybeRefDeep, MaybeRefOrGetter } from './types'
|
||||
|
||||
export type UsePrefetchQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
> = OmitKeyof<
|
||||
QueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
never
|
||||
>,
|
||||
'queryFn'
|
||||
> & {
|
||||
queryFn?: Exclude<
|
||||
QueryExecuteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
never
|
||||
>['queryFn'],
|
||||
SkipToken
|
||||
>
|
||||
}
|
||||
|
||||
function isGetter<T>(value: MaybeRefOrGetter<T>): value is () => T {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* `usePrefetchQuery` does not return anything — it fires a prefetch as a reactive side effect, useful for
|
||||
* kicking off a fetch ahead of the component that will actually render the data with `useQuery`. You can pass
|
||||
* everything to `usePrefetchQuery` that you can pass to `queryClient.query`, though `queryKey` is always
|
||||
* required, and `queryFn` is required unless a default query function has been defined.
|
||||
*
|
||||
* The prefetch is skipped if the query already has any cached state — including a `pending`/`error` state left
|
||||
* over from a previous attempt — so it won't refetch data that's already there or already in flight. It
|
||||
* re-runs whenever a reactive dependency in `options` (built with `queryOptions`, for example) changes.
|
||||
*
|
||||
* Fire this during render, before a suspense boundary that wraps a component using `useQuery`'s `suspense()`
|
||||
* — see the {@link https://tanstack.com/query/latest/docs/framework/vue/guides/suspense | Suspense guide}.
|
||||
*
|
||||
* @param options - A `ref`, plain value, or reactive getter resolving to the {@link UsePrefetchQueryOptions} to
|
||||
* use — everything you can pass to `queryClient.query`.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns `void` — nothing is returned.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { usePrefetchQuery } from '@tanstack/vue-query'
|
||||
* import Posts from './Posts.vue'
|
||||
*
|
||||
* // Fire the prefetch as soon as this component runs, before `Posts` mounts and calls `useQuery`.
|
||||
* usePrefetchQuery({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <Posts />
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function usePrefetchQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
MaybeRefDeep<
|
||||
UsePrefetchQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>
|
||||
>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): void {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
watchEffect(() => {
|
||||
const resolvedOptions = isGetter(options) ? options() : unref(options)
|
||||
const clonedOptions: UsePrefetchQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
> = cloneDeepUnref(resolvedOptions)
|
||||
|
||||
if (!client.getQueryState(clonedOptions.queryKey)) {
|
||||
void client.query(clonedOptions).catch(noop)
|
||||
}
|
||||
})
|
||||
}
|
||||
479
packages/vue-query/src/useQueries.ts
Normal file
479
packages/vue-query/src/useQueries.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { QueriesObserver } from '@tanstack/query-core'
|
||||
import {
|
||||
computed,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
readonly,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
unref,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
|
||||
import { useQueryClient } from './useQueryClient'
|
||||
import { cloneDeepUnref } from './utils'
|
||||
import type { Ref } from 'vue-demi'
|
||||
import type {
|
||||
DefaultError,
|
||||
DefinedQueryObserverResult,
|
||||
QueriesObserverOptions,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
QueryObserverResult,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseQueryOptions } from './queryOptions'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type { DeepUnwrapRef, MaybeRefDeep, ShallowOption } from './types'
|
||||
|
||||
// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
|
||||
// `placeholderData` function does not have a parameter
|
||||
type UseQueryOptionsForUseQueries<
|
||||
TQueryFnData = unknown,
|
||||
TError = unknown,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
|
||||
|
||||
// Avoid TS depth-limit error in case of large array literal
|
||||
type MAXIMUM_DEPTH = 20
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForUseQueries = symbol
|
||||
|
||||
type GetUseQueryOptionsForUseQueries<T> =
|
||||
// Part 1: if UseQueryOptions are already being sent through, then just return T
|
||||
T extends UseQueryOptions
|
||||
? DeepUnwrapRef<T>
|
||||
: // Part 2: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
|
||||
T extends {
|
||||
queryFnData: infer TQueryFnData
|
||||
error?: infer TError
|
||||
data: infer TData
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<unknown, TError, TData>
|
||||
: // Part 3: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
|
||||
T extends [infer TQueryFnData, infer TError, infer TData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData>
|
||||
: // Part 4: responsible for inferring and enforcing type if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
TQueryKey
|
||||
>
|
||||
: T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TQueryFnData,
|
||||
TQueryKey
|
||||
>
|
||||
: // Fallback
|
||||
UseQueryOptionsForUseQueries
|
||||
|
||||
// A defined initialData setting should return a DefinedQueryObserverResult rather than QueryObserverResult
|
||||
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
|
||||
initialData?: infer TInitialData
|
||||
}
|
||||
? unknown extends TInitialData
|
||||
? QueryObserverResult<TData, TError>
|
||||
: TInitialData extends TData
|
||||
? DefinedQueryObserverResult<TData, TError>
|
||||
: TInitialData extends () => infer TInitialDataResult
|
||||
? unknown extends TInitialDataResult
|
||||
? QueryObserverResult<TData, TError>
|
||||
: TInitialDataResult extends TData
|
||||
? DefinedQueryObserverResult<TData, TError>
|
||||
: QueryObserverResult<TData, TError>
|
||||
: QueryObserverResult<TData, TError>
|
||||
: QueryObserverResult<TData, TError>
|
||||
|
||||
type GetUseQueryResult<T> =
|
||||
// Part 1: if using UseQueryOptions then the types are already set
|
||||
T extends UseQueryOptions<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
any,
|
||||
any
|
||||
>
|
||||
? GetDefinedOrUndefinedQueryResult<
|
||||
T,
|
||||
undefined extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Part 2: responsible for mapping explicit type parameter to function result, if object
|
||||
T extends { queryFnData: any; error?: infer TError; data: infer TData }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: // Part 3: responsible for mapping explicit type parameter to function result, if tuple
|
||||
T extends [any, infer TError, infer TData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
|
||||
: // Part 4: responsible for mapping inferred type to results, if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? GetDefinedOrUndefinedQueryResult<
|
||||
T,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? GetDefinedOrUndefinedQueryResult<
|
||||
T,
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Fallback
|
||||
QueryObserverResult
|
||||
|
||||
/**
|
||||
* UseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
|
||||
*/
|
||||
export type UseQueriesOptions<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseQueryOptionsForUseQueries>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseQueryOptionsForUseQueries<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? UseQueriesOptions<
|
||||
[...Tails],
|
||||
[...TResults, GetUseQueryOptionsForUseQueries<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: ReadonlyArray<unknown> extends T
|
||||
? T
|
||||
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type!
|
||||
// use this to infer the param types in the case of Array.map() argument
|
||||
T extends Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
infer TQueryKey
|
||||
>
|
||||
>
|
||||
? Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
>
|
||||
: // Fallback
|
||||
Array<UseQueryOptionsForUseQueries>
|
||||
|
||||
/**
|
||||
* UseQueriesResults reducer recursively maps type param to results
|
||||
*/
|
||||
export type UseQueriesResults<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<QueryObserverResult>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseQueryResult<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? UseQueriesResults<
|
||||
[...Tails],
|
||||
[...TResults, GetUseQueryResult<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: { [K in keyof T]: GetUseQueryResult<T[K]> }
|
||||
|
||||
type UseQueriesOptionsArg<T extends Array<any>> = readonly [
|
||||
...UseQueriesOptions<T>,
|
||||
]
|
||||
|
||||
/**
|
||||
* The `useQueries` composable can be used to fetch a variable number of queries.
|
||||
*
|
||||
* The `queries` option accepts an array of query option objects mostly identical to `useQuery`'s. It may be a
|
||||
* plain array, a `ref`/reactive array (each entry tracked individually), or a getter function — pass a getter
|
||||
* if the array itself (its length, or which queries are in it) depends on other reactive state.
|
||||
*
|
||||
* Having the same query key more than once in the array of query objects may cause some data to be shared
|
||||
* between queries. To avoid this, consider de-duplicating the queries and map the results back to the desired
|
||||
* structure.
|
||||
*
|
||||
* The `combine` option can be used to combine the results of the queries into a single value. The result will
|
||||
* be structurally shared to be as referentially stable as possible.
|
||||
*
|
||||
* `placeholderData` is supported here too, but unlike `useQuery`, it doesn't receive information from
|
||||
* previously rendered queries, because the number of queries can differ between renders.
|
||||
*
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns A `ref` to the combined result. Without `combine`, this is an array with all the query results, in
|
||||
* the same order as the input. When `combine` is provided, this is the value returned by `combine` instead.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQueries } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ ids: Array<number> }>()
|
||||
*
|
||||
* const postQueries = useQueries({
|
||||
* queries: () =>
|
||||
* props.ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* staleTime: Infinity,
|
||||
* })),
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <ul>
|
||||
* <li v-for="(query, index) in postQueries" :key="props.ids[index]">
|
||||
* <span v-if="query.isPending">Loading...</span>
|
||||
* <span v-else-if="query.isError">Error: {{ query.error.message }}</span>
|
||||
* <span v-else>{{ query.data.title }}</span>
|
||||
* </li>
|
||||
* </ul>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Combining results into a single value:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQueries } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ ids: Array<number> }>()
|
||||
*
|
||||
* const combined = useQueries({
|
||||
* queries: () =>
|
||||
* props.ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })),
|
||||
* combine: (postQueries) => ({
|
||||
* data: postQueries.map((query) => query.data),
|
||||
* isPending: postQueries.some((query) => query.isPending),
|
||||
* isError: postQueries.some((query) => query.isError),
|
||||
* }),
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="combined.isPending">Loading...</span>
|
||||
* <span v-else-if="combined.isError">Error!</span>
|
||||
* <ul v-else>
|
||||
* <li v-for="post in combined.data" :key="post.id">{{ post.title }}</li>
|
||||
* </ul>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Typing `select` via {@link queryOptions}. Note that spreading a `queryOptions` result and overriding
|
||||
* `select` inline still falls back to `unknown` — wrap the spread in `queryOptions` again so the override is
|
||||
* resolved before it reaches `useQueries`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { queryOptions, useQueries } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ id: number }>()
|
||||
*
|
||||
* const postOptions = (id: number) =>
|
||||
* queryOptions({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })
|
||||
*
|
||||
* const [{ data: broken }] = useQueries({
|
||||
* queries: () => [
|
||||
* {
|
||||
* ...postOptions(props.id),
|
||||
* // ❌ `data` is `unknown` here
|
||||
* select: (data) => data.title,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
*
|
||||
* const [{ data: fixed }] = useQueries({
|
||||
* queries: () => [
|
||||
* queryOptions({
|
||||
* ...postOptions(props.id),
|
||||
* // ✅ `data` is `Post`
|
||||
* select: (data) => data.title,
|
||||
* }),
|
||||
* ],
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <h1>{{ fixed }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult = UseQueriesResults<T>,
|
||||
>(
|
||||
{
|
||||
queries,
|
||||
...options
|
||||
}: ShallowOption & {
|
||||
queries:
|
||||
| (() => MaybeRefDeep<UseQueriesOptionsArg<T>>)
|
||||
| MaybeRefDeep<UseQueriesOptionsArg<T>>
|
||||
| MaybeRefDeep<
|
||||
readonly [
|
||||
...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> },
|
||||
]
|
||||
>
|
||||
combine?: (result: UseQueriesResults<T>) => TCombinedResult
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): Readonly<Ref<TCombinedResult>> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!getCurrentScope()) {
|
||||
console.warn(
|
||||
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const client = queryClient || useQueryClient()
|
||||
|
||||
const defaultedQueries = computed(() => {
|
||||
const resolvedQueries =
|
||||
typeof queries === 'function'
|
||||
? (queries as () => MaybeRefDeep<UseQueriesOptionsArg<T>>)()
|
||||
: queries
|
||||
// Only unref the top level array.
|
||||
const queriesRaw = unref(resolvedQueries) as ReadonlyArray<any>
|
||||
|
||||
// Unref the rest for each element in the top level array.
|
||||
return queriesRaw.map((queryOptions) => {
|
||||
const clonedOptions = cloneDeepUnref(queryOptions)
|
||||
|
||||
if (typeof clonedOptions.enabled === 'function') {
|
||||
clonedOptions.enabled = queryOptions.enabled()
|
||||
}
|
||||
|
||||
const defaulted = client.defaultQueryOptions(clonedOptions)
|
||||
defaulted._optimisticResults = client.isRestoring?.value
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
|
||||
return defaulted
|
||||
})
|
||||
})
|
||||
|
||||
const observer = new QueriesObserver<TCombinedResult>(
|
||||
client,
|
||||
defaultedQueries.value,
|
||||
options as QueriesObserverOptions<TCombinedResult>,
|
||||
)
|
||||
|
||||
const getOptimisticResult = () => {
|
||||
const [results, getCombinedResult] = observer.getOptimisticResult(
|
||||
defaultedQueries.value,
|
||||
(options as QueriesObserverOptions<TCombinedResult>).combine,
|
||||
)
|
||||
|
||||
return getCombinedResult(
|
||||
results.map((result, index) => {
|
||||
return {
|
||||
...result,
|
||||
refetch: async (...args: Array<any>) => {
|
||||
const [{ [index]: query }] = observer.getOptimisticResult(
|
||||
defaultedQueries.value,
|
||||
(options as QueriesObserverOptions<TCombinedResult>).combine,
|
||||
)
|
||||
|
||||
return query!.refetch(...args)
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const state = shallowRef(getOptimisticResult())
|
||||
|
||||
let unsubscribe = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
if (client.isRestoring) {
|
||||
watch(
|
||||
client.isRestoring,
|
||||
(isRestoring) => {
|
||||
if (!isRestoring) {
|
||||
unsubscribe()
|
||||
unsubscribe = observer.subscribe(() => {
|
||||
state.value = getOptimisticResult()
|
||||
})
|
||||
|
||||
state.value = getOptimisticResult()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
|
||||
watch(defaultedQueries, (queriesValue) => {
|
||||
observer.setQueries(
|
||||
queriesValue,
|
||||
options as QueriesObserverOptions<TCombinedResult>,
|
||||
)
|
||||
state.value = getOptimisticResult()
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
return options.shallow
|
||||
? shallowReadonly(state)
|
||||
: (readonly(state) as Readonly<Ref<TCombinedResult>>)
|
||||
}
|
||||
308
packages/vue-query/src/useQuery.ts
Normal file
308
packages/vue-query/src/useQuery.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { QueryObserver } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type {
|
||||
DefaultError,
|
||||
DefinedQueryObserverResult,
|
||||
QueryKey,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseBaseQueryReturnType } from './useBaseQuery'
|
||||
import type { MaybeRefOrGetter } from './types'
|
||||
import type { QueryClient } from './queryClient'
|
||||
import type {
|
||||
DefinedInitialQueryOptions,
|
||||
UndefinedInitialQueryOptions,
|
||||
UseQueryOptions,
|
||||
} from './queryOptions'
|
||||
|
||||
export type UseQueryReturnType<TData, TError> = UseBaseQueryReturnType<
|
||||
TData,
|
||||
TError
|
||||
>
|
||||
|
||||
export type UseQueryDefinedReturnType<TData, TError> = UseBaseQueryReturnType<
|
||||
TData,
|
||||
TError,
|
||||
DefinedQueryObserverResult<TData, TError>
|
||||
>
|
||||
|
||||
/**
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` or a reactive getter for the array itself, or `ref`s and
|
||||
* reactive getters as individual entries. Other options are read once when passed as a plain value, and stay
|
||||
* reactive when passed as a `ref` or a `computed`.
|
||||
*
|
||||
* @param options - The {@link DefinedInitialQueryOptions} to use — everything you can pass to `useQuery`, with
|
||||
* `initialData` set.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The current query result, typed so that `data` is never `undefined` (`status` never resolves to
|
||||
* `pending` in this overload's type, since `initialData` guarantees data upfront).
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails,
|
||||
* // so the list stays visible alongside the error.
|
||||
* const { data, isError, error } = useQuery({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isError">Error: {{ error.message }}</span>
|
||||
* <ul>
|
||||
* <li v-for="post in data" :key="post.id">{{ post.title }}</li>
|
||||
* </ul>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryDefinedReturnType<TData, TError>
|
||||
|
||||
/**
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` or a reactive getter for the array itself, or `ref`s and
|
||||
* reactive getters as individual entries. Other options are read once when passed as a plain value, and stay
|
||||
* reactive when passed as a `ref` or a `computed`.
|
||||
*
|
||||
* @param options - The {@link UndefinedInitialQueryOptions} to use — everything you can pass to `useQuery`.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The current query result. `status` is `pending` if there is no cached data to display, `error` if
|
||||
* the last fetch attempt failed, or `success` if the query has data to display.
|
||||
*
|
||||
* @example
|
||||
* A query key built from a reactive `ref` — the query refetches whenever `postId` changes:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { ref } from 'vue'
|
||||
* import { useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const postId = ref(1)
|
||||
* const { status, data, error } = useQuery({
|
||||
* queryKey: ['post', postId],
|
||||
* queryFn: () => fetchPost(postId.value),
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="status === 'pending'">Loading...</span>
|
||||
* <span v-else-if="status === 'error'">Error: {{ error.message }}</span>
|
||||
* <h1 v-else>{{ data.title }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* `select` derives whatever `data` a component needs from the cached value, without changing what's
|
||||
* actually stored in the cache — the cache still holds the full `Post[]`, but `data` here is a `number`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const { data, isPending, isError, error } = useQuery({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* select: (posts) => posts.length,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isPending">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <span v-else>{{ data }} posts</span>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A dependent query, only enabled once `postId` is set — use `isLoading`, not `isPending`, so the
|
||||
* loading state doesn't show while the query is disabled:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { ref } from 'vue'
|
||||
* import { useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ postId: number | undefined }>()
|
||||
*
|
||||
* const { data, isLoading, isError, error } = useQuery({
|
||||
* queryKey: ['post', props.postId],
|
||||
* queryFn: () => fetchPost(props.postId!),
|
||||
* enabled: () => props.postId != null,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="props.postId == null">Select a post</span>
|
||||
* <span v-else-if="isLoading">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <h1 v-else>{{ data?.title }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Seeding a detail query from an already-cached list, to skip the loading state:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ postId: number }>()
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const { data, isError, error } = useQuery({
|
||||
* queryKey: ['post', props.postId],
|
||||
* queryFn: () => fetchPost(props.postId),
|
||||
* initialData: () =>
|
||||
* queryClient
|
||||
* .getQueryData<Array<Post>>(['posts'])
|
||||
* ?.find((post) => post.id === props.postId),
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isError">Error: {{ error.message }}</span>
|
||||
* <h1 v-else>{{ data?.title }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Paginated data, keeping the previous page's data visible while the next page loads:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { ref } from 'vue'
|
||||
* import { keepPreviousData, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const page = ref(0)
|
||||
*
|
||||
* const { data, isPlaceholderData, isError, error } = useQuery({
|
||||
* queryKey: ['posts', page],
|
||||
* queryFn: () => fetchPosts(page.value),
|
||||
* placeholderData: keepPreviousData,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="isError">Error: {{ error.message }}</span>
|
||||
* <template v-else>
|
||||
* <ul>
|
||||
* <li v-for="post in data" :key="post.id">{{ post.title }}</li>
|
||||
* </ul>
|
||||
* <button :disabled="isPlaceholderData" @click="page++">Next Page</button>
|
||||
* </template>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryReturnType<TData, TError>
|
||||
|
||||
/**
|
||||
* Fallback overload for options whose `initialData` presence isn't statically known — for example, a
|
||||
* `ref`/reactive object built up conditionally, rather than a plain object literal. Prefer one of the other
|
||||
* overloads when possible, since they infer whether `data` can be `undefined` from `initialData` directly.
|
||||
*
|
||||
* `enabled` tracks reactive dependencies automatically as a `ref`, a plain value, or a reactive getter
|
||||
* (`() => ...`). `queryKey` reacts through a `ref` or a reactive getter for the array itself, or `ref`s and
|
||||
* reactive getters as individual entries.
|
||||
*
|
||||
* When `options` itself is a reactive getter, the whole object is re-evaluated on every change to its
|
||||
* dependencies, so any option inside it — not just `queryKey` and `enabled` — can change over time.
|
||||
*
|
||||
* @param options - A `ref`, plain value, or reactive getter resolving to the {@link UseQueryOptions} to use.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one provided by `VueQueryPlugin`
|
||||
* will be used.
|
||||
* @returns The current query result, with `data` typed as possibly `undefined`.
|
||||
*
|
||||
* @example
|
||||
* Passing a whole-options getter so `staleTime` reacts to a setting stored elsewhere, not just `queryKey`:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ id: number; staleTime: number }>()
|
||||
*
|
||||
* const { data } = useQuery(() => ({
|
||||
* queryKey: ['post', props.id],
|
||||
* queryFn: () => fetchPost(props.id),
|
||||
* staleTime: props.staleTime,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <h1 v-if="data">{{ data.title }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* `skipToken` disables the query in a type-safe way, without a non-null assertion on `props.postId` —
|
||||
* `queryFn` is only ever called when it's defined. The whole-options getter re-evaluates `queryFn` on every
|
||||
* change to `props.postId`. `queryFn` can also be a `computed`, but never a bare getter, since a function
|
||||
* there is the query function itself. `refetch` doesn't work while `queryFn` is `skipToken` — use
|
||||
* `enabled: false` instead if you need to trigger the query manually:
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { skipToken, useQuery } from '@tanstack/vue-query'
|
||||
*
|
||||
* const props = defineProps<{ postId: number | undefined }>()
|
||||
*
|
||||
* const { data, isLoading, isError, error } = useQuery(() => {
|
||||
* const postId = props.postId
|
||||
* return {
|
||||
* queryKey: ['post', postId],
|
||||
* queryFn: postId != null ? () => fetchPost(postId) : skipToken,
|
||||
* }
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <span v-if="props.postId == null">Select a post</span>
|
||||
* <span v-else-if="isLoading">Loading...</span>
|
||||
* <span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
* <h1 v-else>{{ data?.title }}</h1>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryReturnType<TData, TError>
|
||||
|
||||
export function useQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: MaybeRefOrGetter<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
):
|
||||
| UseQueryReturnType<TData, TError>
|
||||
| UseQueryDefinedReturnType<TData, TError> {
|
||||
return useBaseQuery(QueryObserver, options, queryClient)
|
||||
}
|
||||
45
packages/vue-query/src/useQueryClient.ts
Normal file
45
packages/vue-query/src/useQueryClient.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { hasInjectionContext, inject } from 'vue-demi'
|
||||
|
||||
import { getClientKey } from './utils'
|
||||
import type { QueryClient } from './queryClient'
|
||||
|
||||
/**
|
||||
* Retrieves the `QueryClient` installed by `VueQueryPlugin`, via Vue's `inject`. Must be called inside
|
||||
* `setup()` or another function that supports an injection context.
|
||||
*
|
||||
* @param id - The `queryClientKey` passed to `VueQueryPlugin` — only needed when multiple `QueryClient`s are
|
||||
* installed in the same app.
|
||||
* @throws If called outside an injection context, or if no `QueryClient` was installed via `VueQueryPlugin`.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup lang="ts">
|
||||
* import { useQueryClient } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* function invalidate() {
|
||||
* queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useQueryClient(id = ''): QueryClient {
|
||||
// ensures that `inject()` can be used
|
||||
if (!hasInjectionContext()) {
|
||||
throw new Error(
|
||||
'vue-query hooks can only be used inside setup() function or functions that support injection context.',
|
||||
)
|
||||
}
|
||||
|
||||
const key = getClientKey(id)
|
||||
const queryClient = inject<QueryClient>(key)
|
||||
|
||||
if (!queryClient) {
|
||||
throw new Error(
|
||||
"No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.",
|
||||
)
|
||||
}
|
||||
|
||||
return queryClient
|
||||
}
|
||||
130
packages/vue-query/src/utils.ts
Normal file
130
packages/vue-query/src/utils.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { isRef, unref } from 'vue-demi'
|
||||
import type { MaybeRefDeep } from './types'
|
||||
|
||||
/** @internal Base Vue injection key `VueQueryPlugin` provides the `QueryClient` under. */
|
||||
export const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT'
|
||||
|
||||
/** @internal Builds the injection key `useQueryClient`/`VueQueryPlugin` use for a given `queryClientKey`. */
|
||||
export function getClientKey(key?: string) {
|
||||
const suffix = key ? `:${key}` : ''
|
||||
return `${VUE_QUERY_CLIENT}${suffix}`
|
||||
}
|
||||
|
||||
/** @internal Copies each property from `update` onto `state`, in place, for every key already on `state`. */
|
||||
export function updateState(
|
||||
state: Record<string, any>,
|
||||
update: Record<string, any>,
|
||||
): void {
|
||||
Object.keys(state).forEach((key) => {
|
||||
state[key] = update[key]
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function for cloning deep objects where
|
||||
// the level and key is provided to the callback function.
|
||||
function _cloneDeep<T>(
|
||||
value: MaybeRefDeep<T>,
|
||||
customize?: (
|
||||
val: MaybeRefDeep<T>,
|
||||
key: string,
|
||||
level: number,
|
||||
) => T | undefined,
|
||||
currentKey: string = '',
|
||||
currentLevel: number = 0,
|
||||
): T {
|
||||
if (customize) {
|
||||
const result = customize(value, currentKey, currentLevel)
|
||||
if (result === undefined && isRef(value)) {
|
||||
return result as T
|
||||
}
|
||||
if (result !== undefined) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val, index) =>
|
||||
_cloneDeep(val, customize, String(index), currentLevel + 1),
|
||||
) as unknown as T
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && isPlainObject(value)) {
|
||||
const entries = Object.entries(value).map(([key, val]) => [
|
||||
key,
|
||||
_cloneDeep(val, customize, key, currentLevel + 1),
|
||||
])
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
return value as T
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Deep-clones `value`, recursing into arrays and plain objects. `customize`, if provided, can
|
||||
* intercept any node (by key and nesting level) and substitute its own return value instead of recursing
|
||||
* further.
|
||||
*/
|
||||
export function cloneDeep<T>(
|
||||
value: MaybeRefDeep<T>,
|
||||
customize?: (
|
||||
val: MaybeRefDeep<T>,
|
||||
key: string,
|
||||
level: number,
|
||||
) => T | undefined,
|
||||
): T {
|
||||
return _cloneDeep(value, customize)
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Deep-clones `value` like {@link cloneDeep}, additionally unwrapping any `ref`s it encounters (and,
|
||||
* if `unrefGetters` is `true`, calling any functions it encounters and unwrapping their result too). Always
|
||||
* resolves `queryKey` this way, regardless of `unrefGetters` — this is what lets a `queryKey` containing `ref`s
|
||||
* be passed straight through to `@tanstack/query-core`.
|
||||
*/
|
||||
export function cloneDeepUnref<T>(
|
||||
obj: MaybeRefDeep<T>,
|
||||
unrefGetters = false,
|
||||
): T {
|
||||
return cloneDeep(obj, (val, key, level) => {
|
||||
// Check if we're at the top level and the key is 'queryKey'
|
||||
//
|
||||
// If so, take the recursive descent where we resolve
|
||||
// getters to values as well as refs.
|
||||
if (level === 1 && key === 'queryKey') {
|
||||
return cloneDeepUnref(val, true)
|
||||
}
|
||||
|
||||
// Resolve getters to values if specified.
|
||||
if (unrefGetters && isFunction(val)) {
|
||||
// Cast due to older TS versions not allowing calling
|
||||
// on certain intersection types.
|
||||
return cloneDeepUnref((val as Function)(), unrefGetters)
|
||||
}
|
||||
|
||||
// Unref refs and continue to recurse into the value.
|
||||
if (isRef(val)) {
|
||||
return cloneDeepUnref(unref(val), unrefGetters)
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
|
||||
function isPlainObject(value: unknown): value is Object {
|
||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||
return false
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
return prototype === null || prototype === Object.prototype
|
||||
}
|
||||
|
||||
function isFunction(value: unknown): value is Function {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
/** @internal Resolves `source` to a plain value — calls it if it's a function, otherwise deep-unwraps it. */
|
||||
export function toValueDeep<T>(source: (() => T) | MaybeRefDeep<T>): T {
|
||||
return isFunction(source) ? source() : cloneDeepUnref(source)
|
||||
}
|
||||
155
packages/vue-query/src/vueQueryPlugin.ts
Normal file
155
packages/vue-query/src/vueQueryPlugin.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { isVue2 } from 'vue-demi'
|
||||
import { environmentManager } from '@tanstack/query-core'
|
||||
|
||||
import { QueryClient } from './queryClient'
|
||||
import { getClientKey } from './utils'
|
||||
import { setupDevtools } from './devtools/devtools'
|
||||
import type { QueryClientConfig } from './types'
|
||||
|
||||
type ClientPersister = (client: QueryClient) => [() => void, Promise<void>]
|
||||
|
||||
interface CommonOptions {
|
||||
/** Enables the legacy Vue Devtools v6 plugin panel. Prefer `@tanstack/vue-query-devtools` for Vue 3 apps. */
|
||||
enableDevtoolsV6Plugin?: boolean
|
||||
/**
|
||||
* Distinguishes multiple `QueryClient`s provided in the same app — pass the same key to `useQueryClient` to
|
||||
* pick this one. Only needed when installing more than one client.
|
||||
*/
|
||||
queryClientKey?: string
|
||||
/**
|
||||
* Restores a persisted cache before the app mounts. While it runs, `queryClient.isRestoring` is `true`, which
|
||||
* suppresses fetches — see `usePersistQueryClient` in `@tanstack/query-persist-client-core` for building one.
|
||||
*/
|
||||
clientPersister?: ClientPersister
|
||||
/** Called once `clientPersister`'s returned promise resolves and the cache has finished restoring. */
|
||||
clientPersisterOnSuccess?: (client: QueryClient) => void
|
||||
}
|
||||
|
||||
interface ConfigOptions extends CommonOptions {
|
||||
/** Options for the `QueryClient` the plugin constructs. Ignored if `queryClient` is also set. */
|
||||
queryClientConfig?: QueryClientConfig
|
||||
}
|
||||
|
||||
interface ClientOptions extends CommonOptions {
|
||||
/** Use your own `QueryClient` instance instead of having the plugin construct one from `queryClientConfig`. */
|
||||
queryClient?: QueryClient
|
||||
}
|
||||
|
||||
export type VueQueryPluginOptions = ConfigOptions | ClientOptions
|
||||
|
||||
/**
|
||||
* Installs a `QueryClient` on the Vue app, making it available to every descendant component through
|
||||
* `useQueryClient` — the Vue equivalent of React's `QueryClientProvider`, but wired up as an app-level plugin
|
||||
* instead of a wrapping component.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createApp } from 'vue'
|
||||
* import { VueQueryPlugin } from '@tanstack/vue-query'
|
||||
*
|
||||
* const app = createApp(App)
|
||||
* app.use(VueQueryPlugin)
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Pass a `queryClient` you constructed yourself — useful for SSR, where you need a fresh `QueryClient` per
|
||||
* request, or when the same instance also needs to be used outside of Vue components:
|
||||
* ```ts
|
||||
* import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
||||
*
|
||||
* const queryClient = new QueryClient()
|
||||
* app.use(VueQueryPlugin, { queryClient })
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Or pass `queryClientConfig` to let the plugin construct the `QueryClient` for you, with your own defaults:
|
||||
* ```ts
|
||||
* app.use(VueQueryPlugin, {
|
||||
* queryClientConfig: {
|
||||
* defaultOptions: { queries: { staleTime: 5 * 1000 } },
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const VueQueryPlugin = {
|
||||
install: (app: any, options: VueQueryPluginOptions = {}) => {
|
||||
const clientKey = getClientKey(options.queryClientKey)
|
||||
let client: QueryClient
|
||||
|
||||
if ('queryClient' in options && options.queryClient) {
|
||||
client = options.queryClient
|
||||
} else {
|
||||
const clientConfig =
|
||||
'queryClientConfig' in options ? options.queryClientConfig : undefined
|
||||
client = new QueryClient(clientConfig)
|
||||
}
|
||||
|
||||
if (!environmentManager.isServer()) {
|
||||
client.mount()
|
||||
}
|
||||
|
||||
let persisterUnmount = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
if (options.clientPersister) {
|
||||
if (client.isRestoring) {
|
||||
client.isRestoring.value = true
|
||||
}
|
||||
const [unmount, promise] = options.clientPersister(client)
|
||||
persisterUnmount = unmount
|
||||
promise.then(() => {
|
||||
if (client.isRestoring) {
|
||||
client.isRestoring.value = false
|
||||
}
|
||||
options.clientPersisterOnSuccess?.(client)
|
||||
})
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
client.unmount()
|
||||
persisterUnmount()
|
||||
}
|
||||
|
||||
if (app.onUnmount) {
|
||||
app.onUnmount(cleanup)
|
||||
} else {
|
||||
const originalUnmount = app.unmount
|
||||
app.unmount = function vueQueryUnmount() {
|
||||
cleanup()
|
||||
originalUnmount()
|
||||
}
|
||||
}
|
||||
|
||||
if (isVue2) {
|
||||
app.mixin({
|
||||
beforeCreate() {
|
||||
// HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/master/src/apis/inject.ts#L30
|
||||
if (!this._provided) {
|
||||
const provideCache = {}
|
||||
Object.defineProperty(this, '_provided', {
|
||||
get: () => provideCache,
|
||||
set: (v) => Object.assign(provideCache, v),
|
||||
})
|
||||
}
|
||||
|
||||
this._provided[clientKey] = client
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (this === this.$root && options.enableDevtoolsV6Plugin) {
|
||||
setupDevtools(this, client)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
app.provide(clientKey, client)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (options.enableDevtoolsV6Plugin) {
|
||||
setupDevtools(app, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
13
packages/vue-query/test-setup.ts
Normal file
13
packages/vue-query/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
vi.mock('vue-demi', async () => {
|
||||
const vue = await vi.importActual('vue-demi')
|
||||
return {
|
||||
...(vue as any),
|
||||
inject: vi.fn(),
|
||||
provide: vi.fn(),
|
||||
onScopeDispose: vi.fn(),
|
||||
getCurrentInstance: vi.fn(() => ({ proxy: {} })),
|
||||
hasInjectionContext: vi.fn(() => true),
|
||||
}
|
||||
})
|
||||
14
packages/vue-query/tsconfig.json
Normal file
14
packages/vue-query/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist-ts",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test-setup.ts",
|
||||
"*.config.ts",
|
||||
"*.config.js",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
9
packages/vue-query/tsconfig.legacy.json
Normal file
9
packages/vue-query/tsconfig.legacy.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist-ts/legacy"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__mocks__", "src/__tests__"],
|
||||
"references": [{ "path": "../query-core" }]
|
||||
}
|
||||
10
packages/vue-query/tsconfig.prod.json
Normal file
10
packages/vue-query/tsconfig.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"composite": false,
|
||||
"rootDir": "../../"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__tests__"]
|
||||
}
|
||||
7
packages/vue-query/tsdown.config.ts
Normal file
7
packages/vue-query/tsdown.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
import { legacyConfig, modernConfig } from './root.tsdown.config.js'
|
||||
|
||||
export default defineConfig([
|
||||
modernConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }),
|
||||
legacyConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }),
|
||||
])
|
||||
42
packages/vue-query/vite.config.ts
Normal file
42
packages/vue-query/vite.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import packageJson from './package.json'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
environments: {
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: packageJson.name,
|
||||
dir: './src',
|
||||
watch: false,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['test-setup.ts'],
|
||||
coverage: {
|
||||
enabled: !!process.env.CI,
|
||||
provider: 'istanbul',
|
||||
include: ['src/**/*'],
|
||||
exclude: ['src/__tests__/**'],
|
||||
},
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
checker: '../../node_modules/typescript/bin/tsc',
|
||||
},
|
||||
onConsoleLog: function (log) {
|
||||
if (log.includes('Download the Vue Devtools extension')) {
|
||||
return false
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user