snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View 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

View 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>
[![Vue Query logo](https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.png)](https://github.com/TanStack/query/tree/main/packages/vue-query)
[![npm version](https://img.shields.io/npm/v/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query)
[![npm license](https://img.shields.io/npm/l/@tanstack/vue-query)](https://github.com/TanStack/query/blob/main/LICENSE)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query)
[![npm](https://img.shields.io/npm/dm/@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
- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](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,
})
```

View 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']]

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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

View 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
}
}
}

View File

@@ -0,0 +1 @@
../../eslint.config.js

View File

@@ -0,0 +1 @@
../../scripts/getTsdownConfig.js

View 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)

View 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)

View 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>>()
})
})

View 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
>()
})
})

View File

@@ -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)
})
})

View 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'],
})
})
})
})

View 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>()
})
})

View 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'] }),
)
})
})

View 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({})
})
})
})

View 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,
})
})
})

View 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)
})
})
})

View 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>()
})
})

View 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)
})
})

View 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,
}),
)
})
})

View 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)
})
})

View 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()
}
})
})

View 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)
})
})

View 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
>()
},
})
})
})

View 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)
})
})

View 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])
})
})

View File

@@ -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),
}),
)
})
})

View File

@@ -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()
}
})
})

View 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),
}),
)
})
})

View 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()
}
})
})

View 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>()
})
})

View 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)
})
})

View 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>()
})
})
})

View 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'])
})
})
})

View 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)
})
})

View 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)
})
})
})

View 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)
})
})
})

View 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,
},
],
}
}
})
},
)
}

View 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,
}

View 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'

View 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
}

View 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))
}
}

View 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
}

View 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))
}
}

View 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))
}
}

View 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
}

View 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
}

View 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>
}

View 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,
)
}

View 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
}

View 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,
}
}

View 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)
}

View 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)
}
})
}

View 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)
}
})
}

View 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>>)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}
}
}
},
}

View 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),
}
})

View File

@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist-ts",
"rootDir": "."
},
"include": [
"src",
"test-setup.ts",
"*.config.ts",
"*.config.js",
"package.json"
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist-ts/legacy"
},
"include": ["src"],
"exclude": ["src/__mocks__", "src/__tests__"],
"references": [{ "path": "../query-core" }]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": false,
"composite": false,
"rootDir": "../../"
},
"include": ["src"],
"exclude": ["src/__tests__"]
}

View 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'] }),
])

View 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
},
},
})