diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
index eb804d66fa5..c6358167dea 100644
--- a/.codesandbox/ci.json
+++ b/.codesandbox/ci.json
@@ -1,6 +1,6 @@
{
"installCommand": "install:csb",
- "sandboxes": ["/examples/react/basic", "/examples/react/basic-typescript", "/examples/solid/basic-typescript"],
+ "sandboxes": ["/examples/react/basic", "/examples/react/basic-typescript", "/examples/solid/basic-typescript", "/examples/vue/basic"],
"packages": ["packages/**"],
"node": "16"
}
diff --git a/docs/adapters/vue-query.md b/docs/adapters/vue-query.md
index 4bebc13b675..e2dfb6c9130 100644
--- a/docs/adapters/vue-query.md
+++ b/docs/adapters/vue-query.md
@@ -1,7 +1,52 @@
---
-title: Vue Query (Coming Soon)
+title: Vue Query
---
-> ⚠️ This module has not yet been developed. It requires an adapter similar to `react-query` to work. We estimate the amount of code to do this is low-to-moderate, but does require familiarity with the Vue framework. If you would like to contribute this adapter, please open a PR!
+The `vue-query` package offers a 1st-class API for using TanStack Query via Vue. However, all of the primitives you receive from these hooks are core APIs that are shared across all of the TanStack Adapters including the Query Client, query results, query subscriptions, etc.
-The `@tanstack/vue-query` package offers a 1st-class API for using TanStack Query via Vue. However, all of the primitives you receive from this API are core APIs that are shared across all of the TanStack Adapters including the Query Client, query results, query subscriptions, etc.
+## Example
+
+This example very briefly illustrates the 3 core concepts of Vue Query:
+
+- [Queries](guides/queries)
+- [Mutations](guides/mutations)
+- [Query Invalidation](guides/query-invalidation)
+
+```vue
+
+
+
+ Loading...
+ Error: {{ error.message }}
+
+
+ Add Todo
+
+```
+
+These three concepts make up most of the core functionality of Vue Query. The next sections of the documentation will go over each of these core concepts in great detail.
\ No newline at end of file
diff --git a/docs/config.json b/docs/config.json
index 670ce3c920c..76c3c74fce1 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -58,8 +58,8 @@
"to": "adapters/solid-query"
},
{
- "label": "Vue Query (Coming Soon)",
- "to": "#"
+ "label": "Vue Query",
+ "to": "adapters/vue-query"
},
{
"label": "Svelte Query (Coming Soon)",
diff --git a/examples/vue/basic/.gitignore b/examples/vue/basic/.gitignore
new file mode 100644
index 00000000000..d424f6a89a8
--- /dev/null
+++ b/examples/vue/basic/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+package-lock.json
diff --git a/examples/vue/basic/README.md b/examples/vue/basic/README.md
new file mode 100644
index 00000000000..7573cb91da8
--- /dev/null
+++ b/examples/vue/basic/README.md
@@ -0,0 +1,6 @@
+# Basic example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm run dev` or `yarn dev`
diff --git a/examples/vue/basic/index.html b/examples/vue/basic/index.html
new file mode 100644
index 00000000000..547b3c19e69
--- /dev/null
+++ b/examples/vue/basic/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vue Query Example
+
+
+
+
+
+
diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json
new file mode 100644
index 00000000000..faddca4d24a
--- /dev/null
+++ b/examples/vue/basic/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@tanstack/query-example-vue-basic",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "build:dev": "vite build -m development",
+ "serve": "vite preview"
+ },
+ "dependencies": {
+ "vue": "3.2.39",
+ "@tanstack/vue-query": "^4.9.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "3.1.0",
+ "typescript": "4.8.4",
+ "vite": "3.1.4"
+ }
+}
diff --git a/examples/vue/basic/src/App.vue b/examples/vue/basic/src/App.vue
new file mode 100644
index 00000000000..52028d9dcdb
--- /dev/null
+++ b/examples/vue/basic/src/App.vue
@@ -0,0 +1,43 @@
+
+
+
+ Vue Query - Basic
+
+ As you visit the posts below, you will notice them in a loading state the
+ first time you load them. However, after you return to this list and click
+ on any posts you have already visited again, you will see them load
+ instantly and background refresh right before your eyes!
+
+ (You may need to throttle your network speed to simulate longer loading
+ sequences)
+
+
+
+
+
diff --git a/examples/vue/basic/src/Post.vue b/examples/vue/basic/src/Post.vue
new file mode 100644
index 00000000000..bb9f36e0895
--- /dev/null
+++ b/examples/vue/basic/src/Post.vue
@@ -0,0 +1,51 @@
+
+
+
+ Post {{ postId }}
+ Back
+ Loading...
+ An error has occurred: {{ error }}
+
+
{{ data.title }}
+
+
Background Updating...
+
+
+
+
diff --git a/examples/vue/basic/src/Posts.vue b/examples/vue/basic/src/Posts.vue
new file mode 100644
index 00000000000..12ee8ef15e1
--- /dev/null
+++ b/examples/vue/basic/src/Posts.vue
@@ -0,0 +1,55 @@
+
+
+
+ Posts
+ Loading...
+ An error has occurred: {{ error }}
+
+
+
+
diff --git a/examples/vue/basic/src/main.ts b/examples/vue/basic/src/main.ts
new file mode 100644
index 00000000000..7c6ec1d6b81
--- /dev/null
+++ b/examples/vue/basic/src/main.ts
@@ -0,0 +1,6 @@
+import { createApp } from "vue";
+import { VueQueryPlugin } from "@tanstack/vue-query";
+
+import App from "./App.vue";
+
+createApp(App).use(VueQueryPlugin).mount("#app");
diff --git a/examples/vue/basic/src/shims-vue.d.ts b/examples/vue/basic/src/shims-vue.d.ts
new file mode 100644
index 00000000000..daba9b9ec05
--- /dev/null
+++ b/examples/vue/basic/src/shims-vue.d.ts
@@ -0,0 +1,5 @@
+declare module "*.vue" {
+ import { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
diff --git a/examples/vue/basic/src/types.d.ts b/examples/vue/basic/src/types.d.ts
new file mode 100644
index 00000000000..0ff5fbb96df
--- /dev/null
+++ b/examples/vue/basic/src/types.d.ts
@@ -0,0 +1,6 @@
+export interface Post {
+ userId: number;
+ id: number;
+ title: string;
+ body: string;
+}
diff --git a/examples/vue/basic/tsconfig.json b/examples/vue/basic/tsconfig.json
new file mode 100644
index 00000000000..e754e65292f
--- /dev/null
+++ b/examples/vue/basic/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"],
+ "types": ["vite/client"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/examples/vue/basic/vite.config.ts b/examples/vue/basic/vite.config.ts
new file mode 100644
index 00000000000..499a4250d64
--- /dev/null
+++ b/examples/vue/basic/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ optimizeDeps: {
+ include: ["remove-accents"],
+ exclude: ["vue-query", "vue-demi"],
+ },
+});
diff --git a/packages/vue-query/.eslintrc b/packages/vue-query/.eslintrc
new file mode 100644
index 00000000000..c1895932146
--- /dev/null
+++ b/packages/vue-query/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "parserOptions": {
+ "project": "./tsconfig.lint.json",
+ "sourceType": "module"
+ },
+ "rules": {
+ "react-hooks/rules-of-hooks": "off"
+ }
+}
diff --git a/packages/vue-query/README.md b/packages/vue-query/README.md
new file mode 100644
index 00000000000..f8f40096775
--- /dev/null
+++ b/packages/vue-query/README.md
@@ -0,0 +1,83 @@
+[](https://github.com/TanStack/query/tree/main/packages/vue-query)
+
+[](https://www.npmjs.com/package/@tanstack/vue-query)
+[](https://github.com/TanStack/query/blob/main/LICENSE)
+[](https://bundlephobia.com/package/@tanstack/vue-query)
+[](https://www.npmjs.com/package/@tanstack/vue-query)
+
+# Vue Query
+
+Hooks for fetching, caching and updating asynchronous data in Vue.
+
+Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi)
+
+# Documentation
+
+Visit https://tanstack.com/query/v4/docs/adapters/vue-query
+
+# Quick Features
+
+- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
+- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
+- Parallel + Dependent Queries
+- Mutations + Reactive Query Refetching
+- Multi-layer Cache + Automatic Garbage Collection
+- Paginated + Cursor-based Queries
+- Load-More + Infinite Scroll Queries w/ Scroll Recovery
+- Request Cancellation
+- (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching
+- (experimental) SSR support
+- Dedicated Devtools
+- [](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported)
+
+# Quick Start
+
+1. Install `vue-query`
+
+ ```bash
+ $ npm i @tanstack/vue-query
+ # or
+ $ pnpm add @tanstack/vue-query
+ # or
+ $ yarn 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**
+
+ ```ts
+ import { createApp } from "vue";
+ import { VueQueryPlugin } from "vue-query";
+
+ import App from "./App.vue";
+
+ createApp(App).use(VueQueryPlugin).mount("#app");
+ ```
+
+3. Use query
+
+ ```ts
+ import { defineComponent } from "vue";
+ import { useQuery } from "vue-query";
+
+ export default defineComponent({
+ name: "MyComponent",
+ setup() {
+ const query = useQuery(["todos"], getTodos);
+
+ return {
+ query,
+ };
+ },
+ });
+ ```
+
+4. If you need to update options on your query dynamically, make sure to pass them as reactive variables
+
+ ```ts
+ const id = ref(1);
+ const enabled = ref(false);
+
+ const query = useQuery(["todos", id], () => getTodos(id), { enabled });
+ ```
diff --git a/packages/vue-query/__mocks__/vue-demi.ts b/packages/vue-query/__mocks__/vue-demi.ts
new file mode 100644
index 00000000000..dde832fe382
--- /dev/null
+++ b/packages/vue-query/__mocks__/vue-demi.ts
@@ -0,0 +1,9 @@
+const vue = jest.requireActual("vue-demi");
+
+module.exports = {
+ ...vue,
+ inject: jest.fn(),
+ provide: jest.fn(),
+ onScopeDispose: jest.fn(),
+ getCurrentInstance: jest.fn(() => ({ proxy: {} })),
+};
diff --git a/packages/vue-query/media/vue-query.png b/packages/vue-query/media/vue-query.png
new file mode 100644
index 00000000000..9d9b0183c0a
Binary files /dev/null and b/packages/vue-query/media/vue-query.png differ
diff --git a/packages/vue-query/media/vue-query.svg b/packages/vue-query/media/vue-query.svg
new file mode 100644
index 00000000000..46684b66579
--- /dev/null
+++ b/packages/vue-query/media/vue-query.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json
new file mode 100644
index 00000000000..54e3b31df58
--- /dev/null
+++ b/packages/vue-query/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@tanstack/vue-query",
+ "version": "4.8.0",
+ "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue",
+ "author": "Damian Osipiuk",
+ "license": "MIT",
+ "repository": "tanstack/query",
+ "homepage": "https://tanstack.com/query",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "types": "build/lib/index.d.ts",
+ "main": "build/lib/index.js",
+ "module": "build/lib/index.esm.js",
+ "exports": {
+ ".": {
+ "types": "./build/lib/index.d.ts",
+ "import": "./build/lib/index.mjs",
+ "default": "./build/lib/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "scripts": {
+ "clean": "rm -rf ./build",
+ "test:eslint": "../../node_modules/.bin/eslint --ext .ts ./src",
+ "test:jest": "../../node_modules/.bin/jest --config jest.config.js",
+ "test:jest:dev": "pnpm test:jest --watch"
+ },
+ "files": [
+ "build/lib/*",
+ "build/umd/*"
+ ],
+ "devDependencies": {
+ "@vue/composition-api": "1.7.1",
+ "vue": "^3.2.40",
+ "vue2": "npm:vue@2"
+ },
+ "dependencies": {
+ "@tanstack/query-core": "workspace:*",
+ "@vue/devtools-api": "^6.4.2",
+ "match-sorter": "^6.3.1",
+ "vue-demi": "^0.13.11"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.1.2",
+ "vue": "^2.5.0 || ^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+}
diff --git a/packages/vue-query/src/__mocks__/useBaseQuery.ts b/packages/vue-query/src/__mocks__/useBaseQuery.ts
new file mode 100644
index 00000000000..4072f1b692d
--- /dev/null
+++ b/packages/vue-query/src/__mocks__/useBaseQuery.ts
@@ -0,0 +1,3 @@
+const { useBaseQuery: originImpl } = jest.requireActual('../useBaseQuery')
+
+export const useBaseQuery = jest.fn(originImpl)
diff --git a/packages/vue-query/src/__mocks__/useQueryClient.ts b/packages/vue-query/src/__mocks__/useQueryClient.ts
new file mode 100644
index 00000000000..cd7484f8ba6
--- /dev/null
+++ b/packages/vue-query/src/__mocks__/useQueryClient.ts
@@ -0,0 +1,15 @@
+import { QueryClient } from '../queryClient'
+
+const queryClient = new QueryClient({
+ logger: {
+ ...console,
+ error: () => {
+ // Noop
+ },
+ },
+ defaultOptions: {
+ queries: { retry: false, cacheTime: Infinity },
+ },
+})
+
+export const useQueryClient = jest.fn(() => queryClient)
diff --git a/packages/vue-query/src/__tests__/mutationCache.test.ts b/packages/vue-query/src/__tests__/mutationCache.test.ts
new file mode 100644
index 00000000000..dc617dd8632
--- /dev/null
+++ b/packages/vue-query/src/__tests__/mutationCache.test.ts
@@ -0,0 +1,40 @@
+import { ref } from 'vue-demi'
+import { MutationCache as MutationCacheOrigin } from '@tanstack/query-core'
+
+import { MutationCache } from '../mutationCache'
+
+describe('MutationCache', () => {
+ beforeAll(() => {
+ jest.spyOn(MutationCacheOrigin.prototype, 'find')
+ jest.spyOn(MutationCacheOrigin.prototype, 'findAll')
+ })
+
+ describe('find', () => {
+ test('should properly unwrap parameters', async () => {
+ const mutationCache = new MutationCache()
+
+ mutationCache.find({
+ mutationKey: ref(['baz']),
+ })
+
+ expect(MutationCacheOrigin.prototype.find).toBeCalledWith({
+ exact: true,
+ mutationKey: ['baz'],
+ })
+ })
+ })
+
+ describe('findAll', () => {
+ test('should properly unwrap parameters', async () => {
+ const mutationCache = new MutationCache()
+
+ mutationCache.findAll({
+ mutationKey: ref(['baz']),
+ })
+
+ expect(MutationCacheOrigin.prototype.findAll).toBeCalledWith({
+ mutationKey: ['baz'],
+ })
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/queryCache.test.ts b/packages/vue-query/src/__tests__/queryCache.test.ts
new file mode 100644
index 00000000000..7c28b91480e
--- /dev/null
+++ b/packages/vue-query/src/__tests__/queryCache.test.ts
@@ -0,0 +1,54 @@
+import { ref } from 'vue-demi'
+import { QueryCache as QueryCacheOrigin } from '@tanstack/query-core'
+
+import { QueryCache } from '../queryCache'
+
+describe('QueryCache', () => {
+ beforeAll(() => {
+ jest.spyOn(QueryCacheOrigin.prototype, 'find')
+ jest.spyOn(QueryCacheOrigin.prototype, 'findAll')
+ })
+
+ describe('find', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryCache = new QueryCache()
+
+ queryCache.find(['foo', ref('bar')], {
+ queryKey: ref(['baz']),
+ })
+
+ expect(QueryCacheOrigin.prototype.find).toBeCalledWith(['foo', 'bar'], {
+ queryKey: ['baz'],
+ })
+ })
+ })
+
+ describe('findAll', () => {
+ test('should properly unwrap two parameters', async () => {
+ const queryCache = new QueryCache()
+
+ queryCache.findAll(['foo', ref('bar')], {
+ queryKey: ref(['baz']),
+ })
+
+ expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith(
+ ['foo', 'bar'],
+ {
+ queryKey: ['baz'],
+ },
+ )
+ })
+
+ test('should properly unwrap one parameter', async () => {
+ const queryCache = new QueryCache()
+
+ queryCache.findAll({
+ queryKey: ref(['baz']),
+ })
+
+ expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({
+ queryKey: ['baz'],
+ })
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/queryClient.test.ts b/packages/vue-query/src/__tests__/queryClient.test.ts
new file mode 100644
index 00000000000..4f6728d4ae2
--- /dev/null
+++ b/packages/vue-query/src/__tests__/queryClient.test.ts
@@ -0,0 +1,546 @@
+import { ref } from 'vue-demi'
+import { QueryClient as QueryClientOrigin } from '@tanstack/query-core'
+
+import { QueryClient } from '../queryClient'
+
+jest.mock('@tanstack/query-core')
+
+const queryKeyRef = ['foo', ref('bar')]
+const queryKeyUnref = ['foo', 'bar']
+
+const fn = () => 'mock'
+
+describe('QueryCache', () => {
+ // beforeAll(() => {
+ // jest.spyOn(QueryCacheOrigin.prototype, "find");
+ // jest.spyOn(QueryCacheOrigin.prototype, "findAll");
+ // });
+
+ describe('isFetching', () => {
+ test('should properly unwrap 1 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.isFetching({
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith({
+ queryKey: queryKeyUnref,
+ })
+ })
+
+ test('should properly unwrap 2 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.isFetching(queryKeyRef, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('isMutating', () => {
+ test('should properly unwrap 1 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.isMutating({
+ mutationKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.isMutating).toBeCalledWith({
+ mutationKey: queryKeyUnref,
+ })
+ })
+ })
+
+ describe('getQueryData', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getQueryData(queryKeyRef, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.getQueryData).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('getQueriesData', () => {
+ test('should properly unwrap queryKey param', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getQueriesData(queryKeyRef)
+
+ expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith(
+ queryKeyUnref,
+ )
+ })
+
+ test('should properly unwrap filters param', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getQueriesData({ queryKey: queryKeyRef })
+
+ expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({
+ queryKey: queryKeyUnref,
+ })
+ })
+ })
+
+ describe('setQueryData', () => {
+ test('should properly unwrap 3 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setQueryData(queryKeyRef, fn, { updatedAt: ref(3) })
+
+ expect(QueryClientOrigin.prototype.setQueryData).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ { updatedAt: 3 },
+ )
+ })
+ })
+
+ describe('setQueriesData', () => {
+ test('should properly unwrap params with queryKey', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setQueriesData(queryKeyRef, fn, { updatedAt: ref(3) })
+
+ expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ { updatedAt: 3 },
+ )
+ })
+
+ test('should properly unwrap params with filters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, {
+ updatedAt: ref(3),
+ })
+
+ expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith(
+ { queryKey: queryKeyUnref },
+ fn,
+ { updatedAt: 3 },
+ )
+ })
+ })
+
+ describe('getQueryState', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getQueryState(queryKeyRef, { queryKey: queryKeyRef })
+
+ expect(QueryClientOrigin.prototype.getQueryState).toBeCalledWith(
+ queryKeyUnref,
+ { queryKey: queryKeyUnref },
+ )
+ })
+ })
+
+ describe('removeQueries', () => {
+ test('should properly unwrap 1 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.removeQueries({
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith({
+ queryKey: queryKeyUnref,
+ })
+ })
+
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.removeQueries(queryKeyRef, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('resetQueries', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.resetQueries(
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith(
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.resetQueries(
+ queryKeyRef,
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+ })
+
+ describe('cancelQueries', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.cancelQueries(
+ {
+ queryKey: queryKeyRef,
+ },
+ { revert: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith(
+ {
+ queryKey: queryKeyUnref,
+ },
+ { revert: false },
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.cancelQueries(
+ queryKeyRef,
+ {
+ queryKey: queryKeyRef,
+ },
+ { revert: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ { revert: false },
+ )
+ })
+ })
+
+ describe('invalidateQueries', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.invalidateQueries(
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith(
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.invalidateQueries(
+ queryKeyRef,
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+ })
+
+ describe('refetchQueries', () => {
+ test('should properly unwrap 2 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.refetchQueries(
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith(
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.refetchQueries(
+ queryKeyRef,
+ {
+ queryKey: queryKeyRef,
+ },
+ { cancelRefetch: ref(false) },
+ )
+
+ expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ { cancelRefetch: false },
+ )
+ })
+ })
+
+ describe('fetchQuery', () => {
+ test('should properly unwrap 1 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchQuery({
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith({
+ queryKey: queryKeyUnref,
+ })
+ })
+
+ test('should properly unwrap 2 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchQuery(queryKeyRef, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ undefined,
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchQuery(queryKeyRef, fn, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('prefetchQuery', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.prefetchQuery(queryKeyRef, fn, { queryKey: queryKeyRef })
+
+ expect(QueryClientOrigin.prototype.prefetchQuery).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('fetchInfiniteQuery', () => {
+ test('should properly unwrap 1 parameter', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchInfiniteQuery({
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith({
+ queryKey: queryKeyUnref,
+ })
+ })
+
+ test('should properly unwrap 2 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchInfiniteQuery(queryKeyRef, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith(
+ queryKeyUnref,
+ {
+ queryKey: queryKeyUnref,
+ },
+ undefined,
+ )
+ })
+
+ test('should properly unwrap 3 parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.fetchInfiniteQuery(queryKeyRef, fn, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('prefetchInfiniteQuery', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.prefetchInfiniteQuery(queryKeyRef, fn, {
+ queryKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.prefetchInfiniteQuery).toBeCalledWith(
+ queryKeyUnref,
+ fn,
+ {
+ queryKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('setDefaultOptions', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setDefaultOptions({
+ queries: {
+ enabled: ref(false),
+ },
+ })
+
+ expect(QueryClientOrigin.prototype.setDefaultOptions).toBeCalledWith({
+ queries: {
+ enabled: false,
+ },
+ })
+ })
+ })
+
+ describe('setQueryDefaults', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setQueryDefaults(queryKeyRef, {
+ enabled: ref(false),
+ })
+
+ expect(QueryClientOrigin.prototype.setQueryDefaults).toBeCalledWith(
+ queryKeyUnref,
+ {
+ enabled: false,
+ },
+ )
+ })
+ })
+
+ describe('getQueryDefaults', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getQueryDefaults(queryKeyRef)
+
+ expect(QueryClientOrigin.prototype.getQueryDefaults).toBeCalledWith(
+ queryKeyUnref,
+ )
+ })
+ })
+
+ describe('setMutationDefaults', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.setMutationDefaults(queryKeyRef, {
+ mutationKey: queryKeyRef,
+ })
+
+ expect(QueryClientOrigin.prototype.setMutationDefaults).toBeCalledWith(
+ queryKeyUnref,
+ {
+ mutationKey: queryKeyUnref,
+ },
+ )
+ })
+ })
+
+ describe('getMutationDefaults', () => {
+ test('should properly unwrap parameters', async () => {
+ const queryClient = new QueryClient()
+
+ queryClient.getMutationDefaults(queryKeyRef)
+
+ expect(QueryClientOrigin.prototype.getMutationDefaults).toBeCalledWith(
+ queryKeyUnref,
+ )
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/test-utils.ts b/packages/vue-query/src/__tests__/test-utils.ts
new file mode 100644
index 00000000000..fa1efb7e750
--- /dev/null
+++ b/packages/vue-query/src/__tests__/test-utils.ts
@@ -0,0 +1,52 @@
+/* istanbul ignore file */
+
+export function flushPromises(timeout = 0): Promise {
+ return new Promise(function (resolve) {
+ setTimeout(resolve, timeout)
+ })
+}
+
+export function simpleFetcher(): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve('Some data')
+ }, 0)
+ })
+}
+
+export function getSimpleFetcherWithReturnData(returnData: unknown) {
+ return () =>
+ new Promise((resolve) => setTimeout(() => resolve(returnData), 0))
+}
+
+export function infiniteFetcher({
+ pageParam = 0,
+}: {
+ pageParam?: number
+}): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve('data on page ' + pageParam)
+ }, 0)
+ })
+}
+
+export function rejectFetcher(): Promise {
+ return new Promise((_, reject) => {
+ setTimeout(() => {
+ return reject(new Error('Some error'))
+ }, 0)
+ })
+}
+
+export function successMutator(param: T): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve(param)
+ }, 0)
+ })
+}
+
+export function errorMutator(_: T): Promise {
+ return rejectFetcher()
+}
diff --git a/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts
new file mode 100644
index 00000000000..f887f3ce1e1
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts
@@ -0,0 +1,34 @@
+import { infiniteFetcher, flushPromises } from './test-utils'
+import { useInfiniteQuery } from '../useInfiniteQuery'
+
+jest.mock('../useQueryClient')
+
+describe('useQuery', () => {
+ test('should properly execute infinite query', async () => {
+ const { data, fetchNextPage, status } = useInfiniteQuery(
+ ['infiniteQuery'],
+ infiniteFetcher,
+ )
+
+ expect(data.value).toStrictEqual(undefined)
+ expect(status.value).toStrictEqual('loading')
+
+ await flushPromises()
+
+ expect(data.value).toStrictEqual({
+ pageParams: [undefined],
+ pages: ['data on page 0'],
+ })
+ expect(status.value).toStrictEqual('success')
+
+ fetchNextPage({ pageParam: 12 })
+
+ await flushPromises()
+
+ expect(data.value).toStrictEqual({
+ pageParams: [undefined, 12],
+ pages: ['data on page 0', 'data on page 12'],
+ })
+ expect(status.value).toStrictEqual('success')
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useIsFetching.test.ts b/packages/vue-query/src/__tests__/useIsFetching.test.ts
new file mode 100644
index 00000000000..8475c0ab92c
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useIsFetching.test.ts
@@ -0,0 +1,91 @@
+import { onScopeDispose, reactive } from 'vue-demi'
+
+import { flushPromises, simpleFetcher } from './test-utils'
+import { useQuery } from '../useQuery'
+import { parseFilterArgs, useIsFetching } from '../useIsFetching'
+
+jest.mock('../useQueryClient')
+
+describe('useIsFetching', () => {
+ test('should properly return isFetching state', async () => {
+ const { isFetching: isFetchingQuery } = useQuery(
+ ['isFetching1'],
+ simpleFetcher,
+ )
+ useQuery(['isFetching2'], simpleFetcher)
+ const isFetching = useIsFetching()
+
+ expect(isFetchingQuery.value).toStrictEqual(true)
+ expect(isFetching.value).toStrictEqual(2)
+
+ await flushPromises()
+
+ expect(isFetchingQuery.value).toStrictEqual(false)
+ expect(isFetching.value).toStrictEqual(0)
+ })
+
+ test('should stop listening to changes on onScopeDispose', async () => {
+ const onScopeDisposeMock = onScopeDispose as jest.MockedFunction<
+ typeof onScopeDispose
+ >
+ onScopeDisposeMock.mockImplementation((fn) => fn())
+
+ const { status } = useQuery(['onScopeDispose'], simpleFetcher)
+ const isFetching = useIsFetching()
+
+ expect(status.value).toStrictEqual('loading')
+ expect(isFetching.value).toStrictEqual(1)
+
+ await flushPromises()
+
+ expect(status.value).toStrictEqual('loading')
+ expect(isFetching.value).toStrictEqual(1)
+
+ await flushPromises()
+
+ expect(status.value).toStrictEqual('loading')
+ expect(isFetching.value).toStrictEqual(1)
+
+ onScopeDisposeMock.mockReset()
+ })
+
+ test('should properly update filters', async () => {
+ const filter = reactive({ stale: false })
+ useQuery(
+ ['isFetching'],
+ () =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve('Some data')
+ }, 100)
+ }),
+ )
+ const isFetching = useIsFetching(filter)
+
+ expect(isFetching.value).toStrictEqual(0)
+
+ filter.stale = true
+ await flushPromises()
+
+ expect(isFetching.value).toStrictEqual(1)
+
+ await flushPromises(100)
+ })
+
+ describe('parseFilterArgs', () => {
+ test('should default to empty filters', () => {
+ const result = parseFilterArgs(undefined)
+
+ expect(result).toEqual({})
+ })
+
+ test('should merge query key with filters', () => {
+ const filters = { stale: true }
+
+ const result = parseFilterArgs(['key'], filters)
+ const expected = { ...filters, queryKey: ['key'] }
+
+ expect(result).toEqual(expected)
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts
new file mode 100644
index 00000000000..37091bb198b
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts
@@ -0,0 +1,97 @@
+import { onScopeDispose, reactive } from 'vue-demi'
+
+import { flushPromises, successMutator } from './test-utils'
+import { useMutation } from '../useMutation'
+import { parseMutationFilterArgs, useIsMutating } from '../useIsMutating'
+import { useQueryClient } from '../useQueryClient'
+
+jest.mock('../useQueryClient')
+
+describe('useIsMutating', () => {
+ test('should properly return isMutating state', async () => {
+ const mutation = useMutation((params: string) => successMutator(params))
+ const mutation2 = useMutation((params: string) => successMutator(params))
+ const isMutating = useIsMutating()
+
+ expect(isMutating.value).toStrictEqual(0)
+
+ mutation.mutateAsync('a')
+ mutation2.mutateAsync('b')
+
+ await flushPromises()
+
+ expect(isMutating.value).toStrictEqual(2)
+
+ await flushPromises()
+
+ expect(isMutating.value).toStrictEqual(0)
+ })
+
+ test('should stop listening to changes on onScopeDispose', async () => {
+ const onScopeDisposeMock = onScopeDispose as jest.MockedFunction<
+ typeof onScopeDispose
+ >
+ onScopeDisposeMock.mockImplementation((fn) => fn())
+
+ const mutation = useMutation((params: string) => successMutator(params))
+ const mutation2 = useMutation((params: string) => successMutator(params))
+ const isMutating = useIsMutating()
+
+ expect(isMutating.value).toStrictEqual(0)
+
+ mutation.mutateAsync('a')
+ mutation2.mutateAsync('b')
+
+ await flushPromises()
+
+ expect(isMutating.value).toStrictEqual(0)
+
+ await flushPromises()
+
+ expect(isMutating.value).toStrictEqual(0)
+
+ onScopeDisposeMock.mockReset()
+ })
+
+ test('should call `useQueryClient` with a proper `queryClientKey`', async () => {
+ const queryClientKey = 'foo'
+ useIsMutating({ queryClientKey })
+
+ expect(useQueryClient).toHaveBeenCalledWith(queryClientKey)
+ })
+
+ test('should properly update filters', async () => {
+ const filter = reactive({ mutationKey: ['foo'] })
+ const { mutate } = useMutation(['isMutating'], (params: string) =>
+ successMutator(params),
+ )
+ mutate('foo')
+
+ const isMutating = useIsMutating(filter)
+
+ expect(isMutating.value).toStrictEqual(0)
+
+ filter.mutationKey = ['isMutating']
+
+ await flushPromises()
+
+ expect(isMutating.value).toStrictEqual(1)
+ })
+
+ describe('parseMutationFilterArgs', () => {
+ test('should default to empty filters', () => {
+ const result = parseMutationFilterArgs(undefined)
+
+ expect(result).toEqual({})
+ })
+
+ test('should merge mutation key with filters', () => {
+ const filters = { fetching: true }
+
+ const result = parseMutationFilterArgs(['key'], filters)
+ const expected = { ...filters, mutationKey: ['key'] }
+
+ expect(result).toEqual(expected)
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts
new file mode 100644
index 00000000000..f28c74e1895
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useMutation.test.ts
@@ -0,0 +1,289 @@
+import { reactive } from 'vue-demi'
+import { errorMutator, flushPromises, successMutator } from './test-utils'
+import { parseMutationArgs, useMutation } from '../useMutation'
+import { useQueryClient } from '../useQueryClient'
+
+jest.mock('../useQueryClient')
+
+describe('useMutation', () => {
+ test('should be in idle state initially', () => {
+ const mutation = useMutation((params) => successMutator(params))
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: true },
+ isLoading: { value: false },
+ isError: { value: false },
+ isSuccess: { value: false },
+ })
+ })
+
+ test('should change state after invoking mutate', () => {
+ const result = 'Mock data'
+ const mutation = useMutation((params: string) => successMutator(params))
+
+ mutation.mutate(result)
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: false },
+ isLoading: { value: true },
+ isError: { value: false },
+ isSuccess: { value: false },
+ data: { value: undefined },
+ error: { value: null },
+ })
+ })
+
+ test('should return error when request fails', async () => {
+ const mutation = useMutation(errorMutator)
+
+ mutation.mutate()
+
+ await flushPromises(10)
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: false },
+ isLoading: { value: false },
+ isError: { value: true },
+ isSuccess: { value: false },
+ data: { value: undefined },
+ error: { value: Error('Some error') },
+ })
+ })
+
+ test('should return data when request succeeds', async () => {
+ const result = 'Mock data'
+ const mutation = useMutation((params: string) => successMutator(params))
+
+ mutation.mutate(result)
+
+ await flushPromises(10)
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: false },
+ isLoading: { value: false },
+ isError: { value: false },
+ isSuccess: { value: true },
+ data: { value: 'Mock data' },
+ error: { value: null },
+ })
+ })
+
+ test('should update reactive options', async () => {
+ const queryClient = useQueryClient()
+ const mutationCache = queryClient.getMutationCache()
+ const options = reactive({ mutationKey: ['foo'] })
+ const mutation = useMutation(
+ (params: string) => successMutator(params),
+ options,
+ )
+
+ options.mutationKey = ['bar']
+ await flushPromises()
+ mutation.mutate('xyz')
+
+ await flushPromises()
+
+ const mutations = mutationCache.find({ mutationKey: ['bar'] })
+
+ expect(mutations?.options.mutationKey).toEqual(['bar'])
+ })
+
+ test('should reset state after invoking mutation.reset', async () => {
+ const mutation = useMutation((params: string) => errorMutator(params))
+
+ mutation.mutate('')
+
+ await flushPromises(10)
+
+ mutation.reset()
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: true },
+ isLoading: { value: false },
+ isError: { value: false },
+ isSuccess: { value: false },
+ data: { value: undefined },
+ error: { value: null },
+ })
+ })
+
+ describe('side effects', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('should call onMutate when passed as an option', async () => {
+ const onMutate = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params), {
+ onMutate,
+ })
+
+ mutation.mutate('')
+
+ await flushPromises(10)
+
+ expect(onMutate).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onError when passed as an option', async () => {
+ const onError = jest.fn()
+ const mutation = useMutation((params: string) => errorMutator(params), {
+ onError,
+ })
+
+ mutation.mutate('')
+
+ await flushPromises(10)
+
+ expect(onError).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onSuccess when passed as an option', async () => {
+ const onSuccess = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params), {
+ onSuccess,
+ })
+
+ mutation.mutate('')
+
+ await flushPromises(10)
+
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onSettled when passed as an option', async () => {
+ const onSettled = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params), {
+ onSettled,
+ })
+
+ mutation.mutate('')
+
+ await flushPromises(10)
+
+ expect(onSettled).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onError when passed as an argument of mutate function', async () => {
+ const onError = jest.fn()
+ const mutation = useMutation((params: string) => errorMutator(params))
+
+ mutation.mutate('', { onError })
+
+ await flushPromises(10)
+
+ expect(onError).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onSuccess when passed as an argument of mutate function', async () => {
+ const onSuccess = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params))
+
+ mutation.mutate('', { onSuccess })
+
+ await flushPromises(10)
+
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+
+ test('should call onSettled when passed as an argument of mutate function', async () => {
+ const onSettled = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params))
+
+ mutation.mutate('', { onSettled })
+
+ await flushPromises(10)
+
+ expect(onSettled).toHaveBeenCalledTimes(1)
+ })
+
+ test('should fire both onSettled functions', async () => {
+ const onSettled = jest.fn()
+ const onSettledOnFunction = jest.fn()
+ const mutation = useMutation((params: string) => successMutator(params), {
+ onSettled,
+ })
+
+ mutation.mutate('', { onSettled: onSettledOnFunction })
+
+ await flushPromises(10)
+
+ expect(onSettled).toHaveBeenCalledTimes(1)
+ expect(onSettledOnFunction).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('async', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('should resolve properly', async () => {
+ const result = 'Mock data'
+ const mutation = useMutation((params: string) => successMutator(params))
+
+ await expect(mutation.mutateAsync(result)).resolves.toBe(result)
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: false },
+ isLoading: { value: false },
+ isError: { value: false },
+ isSuccess: { value: true },
+ data: { value: 'Mock data' },
+ error: { value: null },
+ })
+ })
+
+ test('should throw on error', async () => {
+ const mutation = useMutation(errorMutator)
+
+ await expect(mutation.mutateAsync()).rejects.toThrowError('Some error')
+
+ expect(mutation).toMatchObject({
+ isIdle: { value: false },
+ isLoading: { value: false },
+ isError: { value: true },
+ isSuccess: { value: false },
+ data: { value: undefined },
+ error: { value: Error('Some error') },
+ })
+ })
+ })
+
+ describe('parseMutationArgs', () => {
+ test('should return the same instance of options', () => {
+ const options = { retry: false }
+ const result = parseMutationArgs(options)
+
+ expect(result).toEqual(options)
+ })
+
+ test('should merge query key with options', () => {
+ const options = { retry: false }
+ const result = parseMutationArgs(['key'], options)
+ const expected = { ...options, mutationKey: ['key'] }
+
+ expect(result).toEqual(expected)
+ })
+
+ test('should merge query fn with options', () => {
+ const options = { retry: false }
+ const result = parseMutationArgs(successMutator, options)
+ const expected = { ...options, mutationFn: successMutator }
+
+ expect(result).toEqual(expected)
+ })
+
+ test('should merge query key and fn with options', () => {
+ const options = { retry: false }
+ const result = parseMutationArgs(['key'], successMutator, options)
+ const expected = {
+ ...options,
+ mutationKey: ['key'],
+ mutationFn: successMutator,
+ }
+
+ expect(result).toEqual(expected)
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts
new file mode 100644
index 00000000000..67f28869c11
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useQueries.test.ts
@@ -0,0 +1,193 @@
+import { onScopeDispose, reactive } from 'vue-demi'
+
+import {
+ flushPromises,
+ rejectFetcher,
+ simpleFetcher,
+ getSimpleFetcherWithReturnData,
+} from './test-utils'
+import { useQueries } from '../useQueries'
+
+jest.mock('../useQueryClient')
+
+describe('useQueries', () => {
+ test('should return result for each query', () => {
+ const queries = [
+ {
+ queryKey: ['key1'],
+ queryFn: simpleFetcher,
+ },
+ {
+ queryKey: ['key2'],
+ queryFn: simpleFetcher,
+ },
+ ]
+ const queriesState = useQueries({ queries })
+
+ expect(queriesState).toMatchObject([
+ {
+ status: 'loading',
+ isLoading: true,
+ isFetching: true,
+ isStale: true,
+ },
+ {
+ status: 'loading',
+ isLoading: true,
+ isFetching: true,
+ isStale: true,
+ },
+ ])
+ })
+
+ test('should resolve to success and update reactive state', async () => {
+ const queries = [
+ {
+ queryKey: ['key11'],
+ queryFn: simpleFetcher,
+ },
+ {
+ queryKey: ['key12'],
+ queryFn: simpleFetcher,
+ },
+ ]
+ const queriesState = useQueries({ queries })
+
+ await flushPromises()
+
+ expect(queriesState).toMatchObject([
+ {
+ status: 'success',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ {
+ status: 'success',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ ])
+ })
+
+ test('should reject one of the queries and update reactive state', async () => {
+ const queries = [
+ {
+ queryKey: ['key21'],
+ queryFn: rejectFetcher,
+ },
+ {
+ queryKey: ['key22'],
+ queryFn: simpleFetcher,
+ },
+ ]
+ const queriesState = useQueries({ queries })
+
+ await flushPromises()
+
+ expect(queriesState).toMatchObject([
+ {
+ status: 'error',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ {
+ status: 'success',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ ])
+ })
+
+ test('should return state for new queries', async () => {
+ const queries = reactive([
+ {
+ queryKey: ['key31'],
+ queryFn: getSimpleFetcherWithReturnData('value31'),
+ },
+ {
+ queryKey: ['key32'],
+ queryFn: getSimpleFetcherWithReturnData('value32'),
+ },
+ {
+ queryKey: ['key33'],
+ queryFn: getSimpleFetcherWithReturnData('value33'),
+ },
+ ])
+ const queriesState = useQueries({ queries })
+
+ await flushPromises()
+
+ queries.splice(
+ 0,
+ queries.length,
+ {
+ queryKey: ['key31'],
+ queryFn: getSimpleFetcherWithReturnData('value31'),
+ },
+ {
+ queryKey: ['key34'],
+ queryFn: getSimpleFetcherWithReturnData('value34'),
+ },
+ )
+
+ await flushPromises()
+ await flushPromises()
+
+ expect(queriesState.length).toEqual(2)
+ expect(queriesState).toMatchObject([
+ {
+ data: 'value31',
+ status: 'success',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ {
+ data: 'value34',
+ status: 'success',
+ isLoading: false,
+ isFetching: false,
+ isStale: true,
+ },
+ ])
+ })
+
+ test('should stop listening to changes on onScopeDispose', async () => {
+ const onScopeDisposeMock = onScopeDispose as jest.MockedFunction<
+ typeof onScopeDispose
+ >
+ onScopeDisposeMock.mockImplementationOnce((fn) => fn())
+
+ const queries = [
+ {
+ queryKey: ['key41'],
+ queryFn: simpleFetcher,
+ },
+ {
+ queryKey: ['key42'],
+ queryFn: simpleFetcher,
+ },
+ ]
+ const queriesState = useQueries({ queries })
+ await flushPromises()
+
+ expect(queriesState).toMatchObject([
+ {
+ status: 'loading',
+ isLoading: true,
+ isFetching: true,
+ isStale: true,
+ },
+ {
+ status: 'loading',
+ isLoading: true,
+ isFetching: true,
+ isStale: true,
+ },
+ ])
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts
new file mode 100644
index 00000000000..456eeb8cf24
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useQuery.test.ts
@@ -0,0 +1,283 @@
+import {
+ computed,
+ reactive,
+ ref,
+ onScopeDispose,
+ getCurrentInstance,
+} from 'vue-demi'
+import { QueryObserver } from '@tanstack/query-core'
+
+import {
+ flushPromises,
+ rejectFetcher,
+ simpleFetcher,
+ getSimpleFetcherWithReturnData,
+} from './test-utils'
+import { useQuery } from '../useQuery'
+import { useBaseQuery } from '../useBaseQuery'
+
+jest.mock('../useQueryClient')
+jest.mock('../useBaseQuery')
+
+describe('useQuery', () => {
+ test('should properly execute query', () => {
+ useQuery(['key0'], simpleFetcher, { staleTime: 1000 })
+
+ expect(useBaseQuery).toBeCalledWith(
+ QueryObserver,
+ ['key0'],
+ simpleFetcher,
+ {
+ staleTime: 1000,
+ },
+ )
+ })
+
+ test('should return loading status initially', () => {
+ const query = useQuery(['key1'], simpleFetcher)
+
+ expect(query).toMatchObject({
+ status: { value: 'loading' },
+ isLoading: { value: true },
+ isFetching: { value: true },
+ isStale: { value: true },
+ })
+ })
+
+ test('should resolve to success and update reactive state: useQuery(key, dataFn)', async () => {
+ const query = useQuery(['key2'], getSimpleFetcherWithReturnData('result2'))
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ data: { value: 'result2' },
+ isLoading: { value: false },
+ isFetching: { value: false },
+ isFetched: { value: true },
+ isSuccess: { value: true },
+ })
+ })
+
+ test('should resolve to success and update reactive state: useQuery(optionsObj)', async () => {
+ const query = useQuery({
+ queryKey: ['key31'],
+ queryFn: getSimpleFetcherWithReturnData('result31'),
+ enabled: true,
+ })
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ data: { value: 'result31' },
+ isLoading: { value: false },
+ isFetching: { value: false },
+ isFetched: { value: true },
+ isSuccess: { value: true },
+ })
+ })
+
+ test('should resolve to success and update reactive state: useQuery(key, optionsObj)', async () => {
+ const query = useQuery(['key32'], {
+ queryFn: getSimpleFetcherWithReturnData('result32'),
+ enabled: true,
+ })
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ data: { value: 'result32' },
+ isLoading: { value: false },
+ isFetching: { value: false },
+ isFetched: { value: true },
+ isSuccess: { value: true },
+ })
+ })
+
+ test('should reject and update reactive state', async () => {
+ const query = useQuery(['key3'], rejectFetcher)
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'error' },
+ data: { value: undefined },
+ error: { value: { message: 'Some error' } },
+ isLoading: { value: false },
+ isFetching: { value: false },
+ isFetched: { value: true },
+ isError: { value: true },
+ failureCount: { value: 1 },
+ })
+ })
+
+ test('should update query on reactive options object change', async () => {
+ const spy = jest.fn()
+ const onSuccess = ref(() => {
+ // Noop
+ })
+ useQuery(
+ ['key6'],
+ simpleFetcher,
+ reactive({
+ onSuccess,
+ staleTime: 1000,
+ }),
+ )
+
+ onSuccess.value = spy
+
+ await flushPromises()
+
+ expect(spy).toBeCalledTimes(1)
+ })
+
+ test('should update query on reactive (Ref) key change', async () => {
+ const secondKeyRef = ref('key7')
+ const query = useQuery(['key6', secondKeyRef], simpleFetcher)
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ })
+
+ secondKeyRef.value = 'key8'
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'loading' },
+ data: { value: undefined },
+ })
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ })
+ })
+
+ test("should update query when an option is passed as Ref and it's changed", async () => {
+ const enabled = ref(false)
+ const query = useQuery(['key9'], simpleFetcher, { enabled })
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ fetchStatus: { value: 'idle' },
+ data: { value: undefined },
+ })
+
+ enabled.value = true
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ fetchStatus: { value: 'fetching' },
+ data: { value: undefined },
+ })
+
+ await flushPromises()
+
+ expect(query).toMatchObject({
+ status: { value: 'success' },
+ })
+ })
+
+ test('should properly execute dependant queries', async () => {
+ const { data } = useQuery(['dependant1'], simpleFetcher)
+
+ const enabled = computed(() => !!data.value)
+
+ const { fetchStatus, status } = useQuery(
+ ['dependant2'],
+ simpleFetcher,
+ reactive({
+ enabled,
+ }),
+ )
+
+ expect(data.value).toStrictEqual(undefined)
+ expect(fetchStatus.value).toStrictEqual('idle')
+
+ await flushPromises()
+
+ expect(data.value).toStrictEqual('Some data')
+ expect(fetchStatus.value).toStrictEqual('fetching')
+
+ await flushPromises()
+
+ expect(fetchStatus.value).toStrictEqual('idle')
+ expect(status.value).toStrictEqual('success')
+ })
+
+ test('should stop listening to changes on onScopeDispose', async () => {
+ const onScopeDisposeMock = onScopeDispose as jest.MockedFunction<
+ typeof onScopeDispose
+ >
+ onScopeDisposeMock.mockImplementationOnce((fn) => fn())
+
+ const { status } = useQuery(['onScopeDispose'], simpleFetcher)
+
+ expect(status.value).toStrictEqual('loading')
+
+ await flushPromises()
+
+ expect(status.value).toStrictEqual('loading')
+
+ await flushPromises()
+
+ expect(status.value).toStrictEqual('loading')
+ })
+
+ describe('suspense', () => {
+ test('should return a Promise', () => {
+ const getCurrentInstanceSpy = getCurrentInstance as jest.Mock
+ getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
+
+ const query = useQuery(['suspense'], simpleFetcher)
+ const result = query.suspense()
+
+ expect(result).toBeInstanceOf(Promise)
+ })
+
+ test('should resolve after being enabled', () => {
+ const getCurrentInstanceSpy = getCurrentInstance as jest.Mock
+ getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
+
+ let afterTimeout = false
+ const isEnabeld = ref(false)
+ const query = useQuery(['suspense'], simpleFetcher, {
+ enabled: isEnabeld,
+ })
+
+ setTimeout(() => {
+ afterTimeout = true
+ isEnabeld.value = true
+ }, 200)
+
+ return query.suspense().then(() => {
+ expect(afterTimeout).toBe(true)
+ })
+ })
+
+ test('should resolve immidiately when stale without refetching', () => {
+ const getCurrentInstanceSpy = getCurrentInstance as jest.Mock
+ getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} }))
+
+ const fetcherSpy = jest.fn(() => simpleFetcher())
+
+ // let afterTimeout = false;
+ const query = useQuery(['suspense'], simpleFetcher, {
+ staleTime: 10000,
+ initialData: 'foo',
+ })
+
+ return query.suspense().then(() => {
+ expect(fetcherSpy).toHaveBeenCalledTimes(0)
+ })
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/useQueryClient.test.ts b/packages/vue-query/src/__tests__/useQueryClient.test.ts
new file mode 100644
index 00000000000..27861adf0e8
--- /dev/null
+++ b/packages/vue-query/src/__tests__/useQueryClient.test.ts
@@ -0,0 +1,49 @@
+import { getCurrentInstance, inject } from 'vue-demi'
+import { useQueryClient } from '../useQueryClient'
+import { VUE_QUERY_CLIENT } from '../utils'
+
+describe('useQueryClient', () => {
+ const injectSpy = inject as jest.Mock
+ const getCurrentInstanceSpy = getCurrentInstance as jest.Mock
+
+ beforeEach(() => {
+ jest.restoreAllMocks()
+ })
+
+ test('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)
+ })
+
+ test('should throw an error when queryClient does not exist in the context', () => {
+ injectSpy.mockReturnValueOnce(undefined)
+
+ expect(useQueryClient).toThrowError()
+ expect(injectSpy).toHaveBeenCalledTimes(1)
+ expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT)
+ })
+
+ test('should throw an error when used outside of setup function', () => {
+ getCurrentInstanceSpy.mockReturnValueOnce(undefined)
+
+ expect(useQueryClient).toThrowError()
+ expect(getCurrentInstanceSpy).toHaveBeenCalledTimes(1)
+ })
+
+ test('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)
+ })
+})
diff --git a/packages/vue-query/src/__tests__/utils.test.ts b/packages/vue-query/src/__tests__/utils.test.ts
new file mode 100644
index 00000000000..0b88356d4bf
--- /dev/null
+++ b/packages/vue-query/src/__tests__/utils.test.ts
@@ -0,0 +1,154 @@
+import { isQueryKey, updateState, cloneDeep, cloneDeepUnref } from '../utils'
+import { reactive, ref } from 'vue-demi'
+
+describe('utils', () => {
+ describe('isQueryKey', () => {
+ test('should detect an array as query key', () => {
+ expect(isQueryKey(['string', 'array'])).toEqual(true)
+ })
+ })
+
+ describe('updateState', () => {
+ test('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)
+ })
+
+ test('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)
+ })
+
+ test('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', () => {
+ test('should copy primitives and functions AS-IS', () => {
+ expect(cloneDeep(3456)).toBe(3456)
+ expect(cloneDeep('theString')).toBe('theString')
+ expect(cloneDeep(null)).toBe(null)
+ })
+
+ test('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'],
+ ]),
+ )
+ })
+
+ test('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 number[])[2]).not.toBe((val[4] as number[])[2]) // { a : 1 }
+ })
+
+ test('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', () => {
+ test('should unref primitives', () => {
+ expect(cloneDeepUnref(ref(34))).toBe(34)
+ expect(cloneDeepUnref(ref('mystr'))).toBe('mystr')
+ })
+
+ test('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]])
+ })
+
+ test('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' },
+ })
+ })
+
+ test('should unref undefined', () => {
+ expect(cloneDeepUnref(ref(undefined))).toBe(undefined)
+ })
+ })
+})
diff --git a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts
new file mode 100644
index 00000000000..1b56e948ae4
--- /dev/null
+++ b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts
@@ -0,0 +1,256 @@
+import type { App, ComponentOptions } from 'vue'
+import { isVue2, isVue3 } from 'vue-demi'
+
+import type { QueryClient } from '../queryClient'
+import { VueQueryPlugin } from '../vueQueryPlugin'
+import { VUE_QUERY_CLIENT } from '../utils'
+import { setupDevtools } from '../devtools/devtools'
+
+jest.mock('../devtools/devtools')
+
+interface TestApp extends App {
+ onUnmount: Function
+ _unmount: Function
+ _mixin: ComponentOptions
+ _provided: Record
+ $root: TestApp
+}
+
+const testIf = (condition: boolean) => (condition ? test : test.skip)
+
+function getAppMock(withUnmountHook = false): TestApp {
+ const mock = {
+ provide: jest.fn(),
+ unmount: jest.fn(),
+ onUnmount: withUnmountHook
+ ? jest.fn((u: Function) => {
+ mock._unmount = u
+ })
+ : undefined,
+ mixin: (m: ComponentOptions): any => {
+ mock._mixin = m
+ },
+ } as unknown as TestApp
+
+ return mock
+}
+
+describe('VueQueryPlugin', () => {
+ beforeEach(() => {
+ window.__VUE_QUERY_CONTEXT__ = undefined
+ })
+
+ describe('devtools', () => {
+ test('should NOT setup devtools', () => {
+ const setupDevtoolsMock = setupDevtools as jest.Mock
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock)
+
+ expect(setupDevtoolsMock).toHaveBeenCalledTimes(0)
+ })
+
+ testIf(isVue2)('should setup devtools', () => {
+ const envCopy = process.env.NODE_ENV
+ process.env.NODE_ENV = 'development'
+ const setupDevtoolsMock = setupDevtools as jest.Mock
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock)
+
+ appMock.$root = appMock
+ appMock._mixin.beforeCreate?.call(appMock)
+ process.env.NODE_ENV = envCopy
+
+ expect(setupDevtoolsMock).toHaveBeenCalledTimes(1)
+ })
+
+ testIf(isVue3)('should setup devtools', () => {
+ const envCopy = process.env.NODE_ENV
+ process.env.NODE_ENV = 'development'
+ const setupDevtoolsMock = setupDevtools as jest.Mock
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock)
+ process.env.NODE_ENV = envCopy
+
+ expect(setupDevtoolsMock).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('when app unmounts', () => {
+ test('should call unmount on each client when onUnmount is missing', () => {
+ const appMock = getAppMock()
+ const customClient = {
+ mount: jest.fn(),
+ unmount: jest.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)
+ })
+
+ test('should call onUnmount if present', () => {
+ const appMock = getAppMock(true)
+ const customClient = {
+ mount: jest.fn(),
+ unmount: jest.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', () => {
+ testIf(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.objectContaining({ defaultOptions: {} }),
+ })
+ })
+
+ testIf(isVue3)('should provide a client with default clientKey', () => {
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock)
+
+ expect(appMock.provide).toHaveBeenCalledWith(
+ VUE_QUERY_CLIENT,
+ expect.objectContaining({ defaultOptions: {} }),
+ )
+ })
+ })
+
+ describe('when called with custom clientKey', () => {
+ testIf(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.objectContaining({
+ defaultOptions: {},
+ }),
+ })
+ })
+
+ testIf(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.objectContaining({ defaultOptions: {} }),
+ )
+ })
+ })
+
+ describe('when called with custom client', () => {
+ testIf(isVue2)('should provide that custom client', () => {
+ const appMock = getAppMock()
+ const customClient = { mount: jest.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,
+ })
+ })
+
+ testIf(isVue3)('should provide that custom client', () => {
+ const appMock = getAppMock()
+ const customClient = { mount: jest.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', () => {
+ testIf(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)
+
+ expect(appMock._provided).toMatchObject({
+ VUE_QUERY_CLIENT: expect.objectContaining(config),
+ })
+ },
+ )
+
+ testIf(isVue3)(
+ 'should instantiate a client with the provided config',
+ () => {
+ const appMock = getAppMock()
+ const config = {
+ defaultOptions: { queries: { enabled: true } },
+ }
+ VueQueryPlugin.install(appMock, {
+ queryClientConfig: config,
+ })
+
+ expect(appMock.provide).toHaveBeenCalledWith(
+ VUE_QUERY_CLIENT,
+ expect.objectContaining(config),
+ )
+ },
+ )
+ })
+
+ describe('when context sharing is enabled', () => {
+ test('should create context if it does not exist', () => {
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock, { contextSharing: true })
+
+ expect(window.__VUE_QUERY_CONTEXT__).toBeTruthy()
+ })
+
+ test('should create context with options if it does not exist', () => {
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock, {
+ contextSharing: true,
+ queryClientConfig: { defaultOptions: { queries: { staleTime: 5000 } } },
+ })
+
+ expect(
+ window.__VUE_QUERY_CONTEXT__?.getDefaultOptions().queries?.staleTime,
+ ).toEqual(5000)
+ })
+
+ test('should use existing context', () => {
+ const customClient = { mount: jest.fn() } as unknown as QueryClient
+ window.__VUE_QUERY_CONTEXT__ = customClient
+ const appMock = getAppMock()
+ VueQueryPlugin.install(appMock, { contextSharing: true })
+
+ expect(customClient.mount).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/packages/vue-query/src/devtools/devtools.ts b/packages/vue-query/src/devtools/devtools.ts
new file mode 100644
index 00000000000..d0c91f35daa
--- /dev/null
+++ b/packages/vue-query/src/devtools/devtools.ts
@@ -0,0 +1,199 @@
+/* istanbul ignore file */
+
+import { setupDevtoolsPlugin } from '@vue/devtools-api'
+import type { CustomInspectorNode } from '@vue/devtools-api'
+import { matchSorter } from 'match-sorter'
+import type { Query } from '@tanstack/query-core'
+import type { QueryClient } from '../queryClient'
+import {
+ getQueryStateLabel,
+ getQueryStatusBg,
+ getQueryStatusFg,
+ sortFns,
+} from './utils'
+
+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://github.com/DamianOsipiuk/vue-query',
+ logo: 'https://vue-query.vercel.app/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]!,
+ },
+ },
+ },
+ (api) => {
+ const queryCache = queryClient.getQueryCache()
+
+ api.addInspector({
+ id: pluginId,
+ label: pluginName,
+ icon: 'api',
+ nodeActions: [
+ {
+ icon: 'cloud_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(query.queryKey)
+ },
+ },
+ {
+ 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)
+ },
+ },
+ ],
+ })
+
+ api.addTimelineLayer({
+ id: pluginId,
+ label: pluginName,
+ color: 0xffd94c,
+ })
+
+ queryCache.subscribe((event) => {
+ api.sendInspectorTree(pluginId)
+ api.sendInspectorState(pluginId)
+
+ if (
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ event &&
+ ['queryAdded', 'queryRemoved', 'queryUpdated'].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.getInspectorTree((payload) => {
+ if (payload.inspectorId === pluginId) {
+ const queries: Query[] = queryCache.getAll()
+ const settings = api.getSettings()
+ const filtered = matchSorter(queries, payload.filter, {
+ keys: ['queryHash'],
+ baseSort: (a, b) =>
+ sortFns[settings.sortFn]!(a.item, b.item) * settings.baseSort,
+ })
+
+ const nodes: CustomInspectorNode[] = filtered.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 as string,
+ },
+ {
+ 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,
+ },
+ ],
+ }
+ }
+ })
+ },
+ )
+}
diff --git a/packages/vue-query/src/devtools/utils.ts b/packages/vue-query/src/devtools/utils.ts
new file mode 100644
index 00000000000..eb96f1755ce
--- /dev/null
+++ b/packages/vue-query/src/devtools/utils.ts
@@ -0,0 +1,99 @@
+/* istanbul ignore file */
+
+import type { Query } from '@tanstack/query-core'
+
+type SortFn = (a: Query, b: Query) => number
+
+// eslint-disable-next-line no-shadow
+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) =>
+ String(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 = {
+ 'Status > Last Updated': statusAndDateSort,
+ 'Query Hash': queryHashSort,
+ 'Last Updated': dateSort,
+}
diff --git a/packages/vue-query/src/index.ts b/packages/vue-query/src/index.ts
new file mode 100644
index 00000000000..5ef0ccfdb7b
--- /dev/null
+++ b/packages/vue-query/src/index.ts
@@ -0,0 +1,26 @@
+/* istanbul ignore file */
+
+export * from '@tanstack/query-core'
+
+export { useQueryClient } from './useQueryClient'
+export { VueQueryPlugin } from './vueQueryPlugin'
+
+export { QueryClient } from './queryClient'
+export { QueryCache } from './queryCache'
+export { MutationCache } from './mutationCache'
+export { useQuery } from './useQuery'
+export { useQueries } from './useQueries'
+export { useInfiniteQuery } from './useInfiniteQuery'
+export { useMutation } from './useMutation'
+export { useIsFetching } from './useIsFetching'
+export { useIsMutating } from './useIsMutating'
+export { VUE_QUERY_CLIENT } from './utils'
+
+export type { UseQueryReturnType } from './useBaseQuery'
+export type { UseQueryOptions } from './useQuery'
+export type { UseInfiniteQueryOptions } from './useInfiniteQuery'
+export type { UseMutationOptions, UseMutationReturnType } from './useMutation'
+export type { UseQueriesOptions, UseQueriesResults } from './useQueries'
+export type { MutationFilters } from './useIsMutating'
+export type { QueryFilters } from './useIsFetching'
+export type { VueQueryPluginOptions } from './vueQueryPlugin'
diff --git a/packages/vue-query/src/mutationCache.ts b/packages/vue-query/src/mutationCache.ts
new file mode 100644
index 00000000000..d12edd522f8
--- /dev/null
+++ b/packages/vue-query/src/mutationCache.ts
@@ -0,0 +1,16 @@
+import { MutationCache as MC } from '@tanstack/query-core'
+import type { Mutation, MutationFilters } from '@tanstack/query-core'
+import type { MaybeRefDeep } from './types'
+import { cloneDeepUnref } from './utils'
+
+export class MutationCache extends MC {
+ find(
+ filters: MaybeRefDeep,
+ ): Mutation | undefined {
+ return super.find(cloneDeepUnref(filters) as MutationFilters)
+ }
+
+ findAll(filters: MaybeRefDeep): Mutation[] {
+ return super.findAll(cloneDeepUnref(filters) as MutationFilters)
+ }
+}
diff --git a/packages/vue-query/src/queryCache.ts b/packages/vue-query/src/queryCache.ts
new file mode 100644
index 00000000000..04cfaad86c2
--- /dev/null
+++ b/packages/vue-query/src/queryCache.ts
@@ -0,0 +1,36 @@
+import { QueryCache as QC } from '@tanstack/query-core'
+import type { Query, QueryKey, QueryFilters } from '@tanstack/query-core'
+import type { MaybeRefDeep } from './types'
+import { cloneDeepUnref, isQueryKey } from './utils'
+
+export class QueryCache extends QC {
+ find(
+ arg1: MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ ): Query | undefined {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters
+ return super.find(arg1Unreffed, arg2Unreffed)
+ }
+
+ findAll(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ ): Query[]
+ findAll(filters?: MaybeRefDeep): Query[]
+ findAll(
+ arg1?: MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ ): Query[]
+ findAll(
+ arg1?: MaybeRefDeep | MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ ): Query[] {
+ const arg1Unreffed = cloneDeepUnref(arg1) as QueryKey | QueryFilters
+ const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters
+ if (isQueryKey(arg1Unreffed)) {
+ return super.findAll(arg1Unreffed, arg2Unreffed)
+ }
+ return super.findAll(arg1Unreffed)
+ }
+}
diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts
new file mode 100644
index 00000000000..27d1d073f5f
--- /dev/null
+++ b/packages/vue-query/src/queryClient.ts
@@ -0,0 +1,568 @@
+import { QueryClient as QC } from '@tanstack/query-core'
+import type {
+ QueryKey,
+ QueryClientConfig,
+ SetDataOptions,
+ ResetQueryFilters,
+ ResetOptions,
+ CancelOptions,
+ InvalidateQueryFilters,
+ InvalidateOptions,
+ RefetchQueryFilters,
+ RefetchOptions,
+ FetchQueryOptions,
+ QueryFunction,
+ FetchInfiniteQueryOptions,
+ InfiniteData,
+ DefaultOptions,
+ QueryObserverOptions,
+ MutationKey,
+ MutationObserverOptions,
+ QueryFilters,
+ MutationFilters,
+ QueryState,
+ Updater,
+} from '@tanstack/query-core'
+import type { MaybeRefDeep } from './types'
+import { cloneDeepUnref, isQueryKey } from './utils'
+import { QueryCache } from './queryCache'
+import { MutationCache } from './mutationCache'
+
+export class QueryClient extends QC {
+ constructor(config: MaybeRefDeep = {}) {
+ const unreffedConfig = cloneDeepUnref(config) as QueryClientConfig
+ const vueQueryConfig: QueryClientConfig = {
+ logger: cloneDeepUnref(unreffedConfig.logger),
+ defaultOptions: cloneDeepUnref(unreffedConfig.defaultOptions),
+ queryCache: unreffedConfig.queryCache || new QueryCache(),
+ mutationCache: unreffedConfig.mutationCache || new MutationCache(),
+ }
+ super(vueQueryConfig)
+ }
+
+ isFetching(filters?: MaybeRefDeep): number
+ isFetching(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ ): number
+ isFetching(
+ arg1?: MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ ): number {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters
+ if (isQueryKey(arg1Unreffed)) {
+ return super.isFetching(arg1Unreffed, arg2Unreffed)
+ }
+ return super.isFetching(arg1Unreffed as QueryFilters)
+ }
+
+ isMutating(filters?: MaybeRefDeep): number {
+ return super.isMutating(cloneDeepUnref(filters) as MutationFilters)
+ }
+
+ getQueryData(
+ queryKey: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ ): TData | undefined {
+ return super.getQueryData(
+ cloneDeepUnref(queryKey),
+ cloneDeepUnref(filters) as QueryFilters,
+ )
+ }
+
+ getQueriesData(
+ queryKey: MaybeRefDeep,
+ ): [QueryKey, TData | undefined][]
+ getQueriesData(
+ filters: MaybeRefDeep,
+ ): [QueryKey, TData | undefined][]
+ getQueriesData(
+ queryKeyOrFilters: MaybeRefDeep | MaybeRefDeep,
+ ): [QueryKey, TData | undefined][] {
+ const unreffed = cloneDeepUnref(queryKeyOrFilters)
+ if (isQueryKey(unreffed)) {
+ return super.getQueriesData(unreffed)
+ }
+ return super.getQueriesData(unreffed as QueryFilters)
+ }
+
+ setQueryData(
+ queryKey: MaybeRefDeep,
+ updater: Updater,
+ options?: MaybeRefDeep,
+ ): TData | undefined {
+ return super.setQueryData(
+ cloneDeepUnref(queryKey),
+ updater,
+ cloneDeepUnref(options) as SetDataOptions,
+ )
+ }
+
+ setQueriesData(
+ queryKey: MaybeRefDeep,
+ updater: Updater,
+ options?: MaybeRefDeep,
+ ): [QueryKey, TData | undefined][]
+ setQueriesData(
+ filters: MaybeRefDeep,
+ updater: Updater,
+ options?: MaybeRefDeep,
+ ): [QueryKey, TData | undefined][]
+ setQueriesData(
+ queryKeyOrFilters: MaybeRefDeep,
+ updater: Updater,
+ options?: MaybeRefDeep,
+ ): [QueryKey, TData | undefined][] {
+ const arg1Unreffed = cloneDeepUnref(queryKeyOrFilters)
+ const arg3Unreffed = cloneDeepUnref(options) as SetDataOptions
+ if (isQueryKey(arg1Unreffed)) {
+ return super.setQueriesData(arg1Unreffed, updater, arg3Unreffed)
+ }
+ return super.setQueriesData(
+ arg1Unreffed as QueryFilters,
+ updater,
+ arg3Unreffed,
+ )
+ }
+
+ getQueryState(
+ queryKey: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ ): QueryState | undefined {
+ return super.getQueryState(
+ cloneDeepUnref(queryKey),
+ cloneDeepUnref(filters) as QueryFilters,
+ )
+ }
+
+ removeQueries(filters?: MaybeRefDeep): void
+ removeQueries(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ ): void
+ removeQueries(
+ arg1?: MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ ): void {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.removeQueries(
+ arg1Unreffed,
+ cloneDeepUnref(arg2) as QueryFilters,
+ )
+ }
+ return super.removeQueries(arg1Unreffed as QueryFilters)
+ }
+
+ resetQueries(
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ resetQueries(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ resetQueries(
+ arg1?: MaybeRefDeep>,
+ arg2?: MaybeRefDeep | ResetOptions>,
+ arg3?: MaybeRefDeep,
+ ): Promise {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.resetQueries(
+ arg1Unreffed,
+ arg2Unreffed as ResetQueryFilters | undefined,
+ cloneDeepUnref(arg3) as ResetOptions,
+ )
+ }
+ return super.resetQueries(
+ arg1Unreffed as ResetQueryFilters,
+ arg2Unreffed as ResetOptions,
+ )
+ }
+
+ cancelQueries(
+ filters?: MaybeRefDeep,
+ options?: MaybeRefDeep,
+ ): Promise
+ cancelQueries(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep,
+ options?: MaybeRefDeep,
+ ): Promise
+ cancelQueries(
+ arg1?: MaybeRefDeep,
+ arg2?: MaybeRefDeep,
+ arg3?: MaybeRefDeep,
+ ): Promise {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.cancelQueries(
+ arg1Unreffed,
+ arg2Unreffed as QueryFilters | undefined,
+ cloneDeepUnref(arg3) as CancelOptions,
+ )
+ }
+ return super.cancelQueries(
+ arg1Unreffed as QueryFilters,
+ arg2Unreffed as CancelOptions,
+ )
+ }
+
+ invalidateQueries(
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ invalidateQueries(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ invalidateQueries(
+ arg1?: MaybeRefDeep>,
+ arg2?: MaybeRefDeep | InvalidateOptions>,
+ arg3?: MaybeRefDeep,
+ ): Promise {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.invalidateQueries(
+ arg1Unreffed,
+ arg2Unreffed as InvalidateQueryFilters | undefined,
+ cloneDeepUnref(arg3) as InvalidateOptions,
+ )
+ }
+ return super.invalidateQueries(
+ arg1Unreffed as InvalidateQueryFilters,
+ arg2Unreffed as InvalidateOptions,
+ )
+ }
+
+ refetchQueries(
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ refetchQueries(
+ queryKey?: MaybeRefDeep,
+ filters?: MaybeRefDeep>,
+ options?: MaybeRefDeep,
+ ): Promise
+ refetchQueries(
+ arg1?: MaybeRefDeep>,
+ arg2?: MaybeRefDeep | RefetchOptions>,
+ arg3?: MaybeRefDeep,
+ ): Promise {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.refetchQueries(
+ arg1Unreffed,
+ arg2Unreffed as RefetchQueryFilters | undefined,
+ cloneDeepUnref(arg3) as RefetchOptions,
+ )
+ }
+ return super.refetchQueries(
+ arg1Unreffed as RefetchQueryFilters,
+ arg2Unreffed as RefetchOptions,
+ )
+ }
+
+ fetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ options: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ fetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ options?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ fetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ queryFn: QueryFunction,
+ options?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ fetchQuery<
+ TQueryFnData,
+ TError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ arg1:
+ | MaybeRefDeep
+ | MaybeRefDeep>,
+ arg2?:
+ | QueryFunction
+ | MaybeRefDeep>,
+ arg3?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.fetchQuery(
+ arg1Unreffed as TQueryKey,
+ arg2Unreffed as QueryFunction,
+ cloneDeepUnref(arg3) as FetchQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey
+ >,
+ )
+ }
+ return super.fetchQuery(
+ arg1Unreffed as FetchQueryOptions,
+ )
+ }
+
+ prefetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ options: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ prefetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ options?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ prefetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ queryFn: QueryFunction,
+ options?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise
+ prefetchQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ arg1: MaybeRefDeep<
+ TQueryKey | FetchQueryOptions
+ >,
+ arg2?:
+ | QueryFunction
+ | MaybeRefDeep>,
+ arg3?: MaybeRefDeep<
+ FetchQueryOptions
+ >,
+ ): Promise {
+ return super.prefetchQuery(
+ cloneDeepUnref(arg1) as any,
+ cloneDeepUnref(arg2) as any,
+ cloneDeepUnref(arg3) as any,
+ )
+ }
+
+ fetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ options: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise>
+ fetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ options?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise>
+ fetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ queryFn: QueryFunction,
+ options?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise>
+ fetchInfiniteQuery<
+ TQueryFnData,
+ TError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ arg1: MaybeRefDeep<
+ | TQueryKey
+ | FetchInfiniteQueryOptions
+ >,
+ arg2?:
+ | QueryFunction
+ | MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ arg3?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise> {
+ const arg1Unreffed = cloneDeepUnref(arg1)
+ const arg2Unreffed = cloneDeepUnref(arg2)
+ if (isQueryKey(arg1Unreffed)) {
+ return super.fetchInfiniteQuery(
+ arg1Unreffed as TQueryKey,
+ arg2Unreffed as QueryFunction,
+ cloneDeepUnref(arg3) as FetchInfiniteQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey
+ >,
+ )
+ }
+ return super.fetchInfiniteQuery(
+ arg1Unreffed as FetchInfiniteQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey
+ >,
+ )
+ }
+
+ prefetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ options: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise
+ prefetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ options?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise
+ prefetchInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ queryKey: MaybeRefDeep,
+ queryFn: QueryFunction,
+ options?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise
+ prefetchInfiniteQuery<
+ TQueryFnData,
+ TError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+ >(
+ arg1: MaybeRefDeep<
+ | TQueryKey
+ | FetchInfiniteQueryOptions
+ >,
+ arg2?:
+ | QueryFunction
+ | MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ arg3?: MaybeRefDeep<
+ FetchInfiniteQueryOptions
+ >,
+ ): Promise {
+ return super.prefetchInfiniteQuery(
+ cloneDeepUnref(arg1) as any,
+ cloneDeepUnref(arg2) as any,
+ cloneDeepUnref(arg3) as any,
+ )
+ }
+
+ setDefaultOptions(options: MaybeRefDeep): void {
+ super.setDefaultOptions(cloneDeepUnref(options) as DefaultOptions)
+ }
+
+ setQueryDefaults(
+ queryKey: MaybeRefDeep,
+ options: MaybeRefDeep>,
+ ): void {
+ super.setQueryDefaults(
+ cloneDeepUnref(queryKey),
+ cloneDeepUnref(options) as any,
+ )
+ }
+
+ getQueryDefaults(
+ queryKey?: MaybeRefDeep,
+ ): QueryObserverOptions | undefined {
+ return super.getQueryDefaults(cloneDeepUnref(queryKey))
+ }
+
+ setMutationDefaults(
+ mutationKey: MaybeRefDeep,
+ options: MaybeRefDeep>,
+ ): void {
+ super.setMutationDefaults(
+ cloneDeepUnref(mutationKey),
+ cloneDeepUnref(options) as any,
+ )
+ }
+
+ getMutationDefaults(
+ mutationKey?: MaybeRefDeep,
+ ): MutationObserverOptions | undefined {
+ return super.getMutationDefaults(cloneDeepUnref(mutationKey))
+ }
+}
diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts
new file mode 100644
index 00000000000..4df6af1c17c
--- /dev/null
+++ b/packages/vue-query/src/types.ts
@@ -0,0 +1,91 @@
+import type {
+ QueryKey,
+ QueryObserverOptions,
+ InfiniteQueryObserverOptions,
+} from '@tanstack/query-core'
+import type { Ref, UnwrapRef } from 'vue-demi'
+import type { QueryClient } from './queryClient'
+
+export type MaybeRef = Ref | T
+export type MaybeRefDeep = T extends Function
+ ? T
+ : MaybeRef<
+ T extends object
+ ? {
+ [Property in keyof T]: MaybeRefDeep
+ }
+ : T
+ >
+
+export type WithQueryClientKey = T & {
+ queryClientKey?: string
+ queryClient?: QueryClient
+}
+
+// A Vue version of QueriesObserverOptions from "@tanstack/query-core"
+// Accept refs as options
+export type VueQueryObserverOptions<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+> = {
+ [Property in keyof QueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >]: Property extends 'queryFn'
+ ? QueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ UnwrapRef
+ >[Property]
+ : MaybeRef<
+ QueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >[Property]
+ >
+}
+
+// A Vue version of InfiniteQueryObserverOptions from "@tanstack/query-core"
+// Accept refs as options
+export type VueInfiniteQueryObserverOptions<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = unknown,
+ TQueryData = unknown,
+ TQueryKey extends QueryKey = QueryKey,
+> = {
+ [Property in keyof InfiniteQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >]: Property extends 'queryFn'
+ ? InfiniteQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ UnwrapRef
+ >[Property]
+ : MaybeRef<
+ InfiniteQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey
+ >[Property]
+ >
+}
diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts
new file mode 100644
index 00000000000..951bbb0c3c7
--- /dev/null
+++ b/packages/vue-query/src/useBaseQuery.ts
@@ -0,0 +1,122 @@
+import { onScopeDispose, toRefs, readonly, reactive, watch } from 'vue-demi'
+import type { ToRefs, UnwrapRef } from 'vue-demi'
+import type {
+ QueryObserver,
+ QueryKey,
+ QueryObserverOptions,
+ QueryObserverResult,
+ QueryFunction,
+} from '@tanstack/query-core'
+import { useQueryClient } from './useQueryClient'
+import { updateState, isQueryKey, cloneDeepUnref } from './utils'
+import type { WithQueryClientKey } from './types'
+import type { UseQueryOptions } from './useQuery'
+import type { UseInfiniteQueryOptions } from './useInfiniteQuery'
+
+export type UseQueryReturnType<
+ TData,
+ TError,
+ Result = QueryObserverResult,
+> = ToRefs> & {
+ suspense: () => Promise
+}
+
+type UseQueryOptionsGeneric<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey extends QueryKey = QueryKey,
+> =
+ | UseQueryOptions
+ | UseInfiniteQueryOptions
+
+export function useBaseQuery<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryData,
+ TQueryKey extends QueryKey,
+>(
+ Observer: typeof QueryObserver,
+ arg1:
+ | TQueryKey
+ | UseQueryOptionsGeneric,
+ arg2:
+ | QueryFunction>
+ | UseQueryOptionsGeneric = {},
+ arg3: UseQueryOptionsGeneric = {},
+): UseQueryReturnType {
+ const options = getQueryUnreffedOptions()
+ const queryClient =
+ options.queryClient ?? useQueryClient(options.queryClientKey)
+ const defaultedOptions = queryClient.defaultQueryOptions(options)
+ const observer = new Observer(queryClient, defaultedOptions)
+ const state = reactive(observer.getCurrentResult())
+ const unsubscribe = observer.subscribe((result) => {
+ updateState(state, result)
+ })
+
+ watch(
+ [() => arg1, () => arg2, () => arg3],
+ () => {
+ observer.setOptions(
+ queryClient.defaultQueryOptions(getQueryUnreffedOptions()),
+ )
+ },
+ { deep: true },
+ )
+
+ onScopeDispose(() => {
+ unsubscribe()
+ })
+
+ const suspense = () => {
+ return new Promise>((resolve) => {
+ const run = () => {
+ const newOptions = queryClient.defaultQueryOptions(
+ getQueryUnreffedOptions(),
+ )
+ if (newOptions.enabled !== false) {
+ const optimisticResult = observer.getOptimisticResult(newOptions)
+ if (optimisticResult.isStale) {
+ resolve(observer.fetchOptimistic(defaultedOptions))
+ } else {
+ resolve(optimisticResult)
+ }
+ }
+ }
+
+ run()
+
+ watch([() => arg1, () => arg2, () => arg3], run, { deep: true })
+ })
+ }
+
+ return {
+ ...(toRefs(readonly(state)) as UseQueryReturnType),
+ suspense,
+ }
+
+ /**
+ * Get Query Options object
+ * All inner refs unwrapped. No Reactivity
+ */
+ function getQueryUnreffedOptions() {
+ let mergedOptions
+
+ if (!isQueryKey(arg1)) {
+ // `useQuery(optionsObj)`
+ mergedOptions = arg1
+ } else if (typeof arg2 === 'function') {
+ // `useQuery(queryKey, queryFn, optionsObj?)`
+ mergedOptions = { ...arg3, queryKey: arg1, queryFn: arg2 }
+ } else {
+ // `useQuery(queryKey, optionsObj?)`
+ mergedOptions = { ...arg2, queryKey: arg1 }
+ }
+
+ return cloneDeepUnref(mergedOptions) as WithQueryClientKey<
+ QueryObserverOptions
+ >
+ }
+}
diff --git a/packages/vue-query/src/useInfiniteQuery.ts b/packages/vue-query/src/useInfiniteQuery.ts
new file mode 100644
index 00000000000..b102acac158
--- /dev/null
+++ b/packages/vue-query/src/useInfiniteQuery.ts
@@ -0,0 +1,114 @@
+import { InfiniteQueryObserver } from '@tanstack/query-core'
+import type { UnwrapRef } from 'vue-demi'
+import type {
+ QueryObserver,
+ QueryFunction,
+ QueryKey,
+ InfiniteQueryObserverResult,
+} from '@tanstack/query-core'
+
+import { useBaseQuery } from './useBaseQuery'
+import type { UseQueryReturnType } from './useBaseQuery'
+
+import type {
+ WithQueryClientKey,
+ VueInfiniteQueryObserverOptions,
+} from './types'
+
+export type UseInfiniteQueryOptions<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+> = WithQueryClientKey<
+ VueInfiniteQueryObserverOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryFnData,
+ TQueryKey
+ >
+>
+
+type InfiniteQueryReturnType = UseQueryReturnType<
+ TData,
+ TError,
+ InfiniteQueryObserverResult
+>
+type UseInfiniteQueryReturnType = Omit<
+ InfiniteQueryReturnType,
+ 'fetchNextPage' | 'fetchPreviousPage' | 'refetch' | 'remove'
+> & {
+ fetchNextPage: InfiniteQueryObserverResult['fetchNextPage']
+ fetchPreviousPage: InfiniteQueryObserverResult<
+ TData,
+ TError
+ >['fetchPreviousPage']
+ refetch: InfiniteQueryObserverResult['refetch']
+ remove: InfiniteQueryObserverResult['remove']
+}
+
+export function useInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ options: UseInfiniteQueryOptions,
+): UseInfiniteQueryReturnType
+
+export function useInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ queryKey: TQueryKey,
+ options?: Omit<
+ UseInfiniteQueryOptions,
+ 'queryKey'
+ >,
+): UseInfiniteQueryReturnType
+
+export function useInfiniteQuery<
+ TQueryFnData = unknown,
+ TError = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ queryKey: TQueryKey,
+ queryFn: QueryFunction>,
+ options?: Omit<
+ UseInfiniteQueryOptions,
+ 'queryKey' | 'queryFn'
+ >,
+): UseInfiniteQueryReturnType
+
+export function useInfiniteQuery<
+ TQueryFnData,
+ TError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ arg1:
+ | TQueryKey
+ | UseInfiniteQueryOptions,
+ arg2?:
+ | QueryFunction>
+ | UseInfiniteQueryOptions,
+ arg3?: UseInfiniteQueryOptions,
+): UseInfiniteQueryReturnType {
+ const result = useBaseQuery(
+ InfiniteQueryObserver as typeof QueryObserver,
+ arg1,
+ arg2,
+ arg3,
+ ) as InfiniteQueryReturnType
+ return {
+ ...result,
+ fetchNextPage: result.fetchNextPage.value,
+ fetchPreviousPage: result.fetchPreviousPage.value,
+ refetch: result.refetch.value,
+ remove: result.remove.value,
+ }
+}
diff --git a/packages/vue-query/src/useIsFetching.ts b/packages/vue-query/src/useIsFetching.ts
new file mode 100644
index 00000000000..43eebbaa61b
--- /dev/null
+++ b/packages/vue-query/src/useIsFetching.ts
@@ -0,0 +1,59 @@
+import { onScopeDispose, ref, watch } from 'vue-demi'
+import type { Ref } from 'vue-demi'
+import type { QueryKey, QueryFilters as QF } from '@tanstack/query-core'
+
+import { useQueryClient } from './useQueryClient'
+import { cloneDeepUnref, isQueryKey } from './utils'
+import type { MaybeRefDeep, WithQueryClientKey } from './types'
+
+export type QueryFilters = MaybeRefDeep>
+
+export function useIsFetching(filters?: QueryFilters): Ref
+export function useIsFetching(
+ queryKey?: QueryKey,
+ filters?: QueryFilters,
+): Ref
+export function useIsFetching(
+ arg1?: QueryKey | QueryFilters,
+ arg2?: QueryFilters,
+): Ref {
+ const filters = ref(parseFilterArgs(arg1, arg2))
+ const queryClient =
+ filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey)
+
+ const isFetching = ref(queryClient.isFetching(filters))
+
+ const unsubscribe = queryClient.getQueryCache().subscribe(() => {
+ isFetching.value = queryClient.isFetching(filters)
+ })
+
+ watch(
+ [() => arg1, () => arg2],
+ () => {
+ filters.value = parseFilterArgs(arg1, arg2)
+ isFetching.value = queryClient.isFetching(filters)
+ },
+ { deep: true },
+ )
+
+ onScopeDispose(() => {
+ unsubscribe()
+ })
+
+ return isFetching
+}
+
+export function parseFilterArgs(
+ arg1?: QueryKey | QueryFilters,
+ arg2: QueryFilters = {},
+) {
+ let options: QueryFilters
+
+ if (isQueryKey(arg1)) {
+ options = { ...arg2, queryKey: arg1 }
+ } else {
+ options = arg1 || {}
+ }
+
+ return cloneDeepUnref(options) as WithQueryClientKey
+}
diff --git a/packages/vue-query/src/useIsMutating.ts b/packages/vue-query/src/useIsMutating.ts
new file mode 100644
index 00000000000..7e4413ffdad
--- /dev/null
+++ b/packages/vue-query/src/useIsMutating.ts
@@ -0,0 +1,59 @@
+import { onScopeDispose, ref, watch } from 'vue-demi'
+import type { Ref } from 'vue-demi'
+import type { MutationKey, MutationFilters as MF } from '@tanstack/query-core'
+
+import { useQueryClient } from './useQueryClient'
+import { cloneDeepUnref, isQueryKey } from './utils'
+import type { MaybeRefDeep, WithQueryClientKey } from './types'
+
+export type MutationFilters = MaybeRefDeep>
+
+export function useIsMutating(filters?: MutationFilters): Ref
+export function useIsMutating(
+ mutationKey?: MutationKey,
+ filters?: Omit,
+): Ref
+export function useIsMutating(
+ arg1?: MutationKey | MutationFilters,
+ arg2?: Omit,
+): Ref {
+ const filters = ref(parseMutationFilterArgs(arg1, arg2))
+ const queryClient =
+ filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey)
+
+ const isMutating = ref(queryClient.isMutating(filters))
+
+ const unsubscribe = queryClient.getMutationCache().subscribe(() => {
+ isMutating.value = queryClient.isMutating(filters)
+ })
+
+ watch(
+ [() => arg1, () => arg2],
+ () => {
+ filters.value = parseMutationFilterArgs(arg1, arg2)
+ isMutating.value = queryClient.isMutating(filters)
+ },
+ { deep: true },
+ )
+
+ onScopeDispose(() => {
+ unsubscribe()
+ })
+
+ return isMutating
+}
+
+export function parseMutationFilterArgs(
+ arg1?: MutationKey | MutationFilters,
+ arg2: MutationFilters = {},
+) {
+ let options: MutationFilters
+
+ if (isQueryKey(arg1)) {
+ options = { ...arg2, mutationKey: arg1 }
+ } else {
+ options = arg1 || {}
+ }
+
+ return cloneDeepUnref(options) as WithQueryClientKey
+}
diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts
new file mode 100644
index 00000000000..07fd73ffda8
--- /dev/null
+++ b/packages/vue-query/src/useMutation.ts
@@ -0,0 +1,189 @@
+import { onScopeDispose, reactive, readonly, toRefs, watch } from 'vue-demi'
+import type { ToRefs } from 'vue-demi'
+import { MutationObserver } from '@tanstack/query-core'
+import type {
+ MutateOptions,
+ MutationFunction,
+ MutationKey,
+ MutationObserverOptions,
+ MutateFunction,
+ MutationObserverResult,
+} from '@tanstack/query-core'
+import { cloneDeepUnref, isQueryKey, updateState } from './utils'
+import { useQueryClient } from './useQueryClient'
+import type { WithQueryClientKey } from './types'
+
+type MutationResult = Omit<
+ MutationObserverResult,
+ 'mutate' | 'reset'
+>
+
+export type UseMutationOptions =
+ WithQueryClientKey<
+ MutationObserverOptions
+ >
+
+type MutateSyncFunction<
+ TData = unknown,
+ TError = unknown,
+ TVariables = void,
+ TContext = unknown,
+> = (
+ ...options: Parameters>
+) => void
+
+export type UseMutationReturnType<
+ TData,
+ TError,
+ TVariables,
+ TContext,
+ Result = MutationResult,
+> = ToRefs> & {
+ mutate: MutateSyncFunction