Repository: urql-graphql/urql
Pull request #3619 (2024-06-18 17:17:28)

feat(vue): refactor composable functions
Resolves https://github.com/urql-graphql/urql/issues/3507
Resolves https://github.com/urql-graphql/urql/issues/3633

## Summary

This includes https://github.com/urql-graphql/urql/pull/3612, but fixed reactivity loosing here.

In addition, a little refactoring for `use*` composable functions for reducing code duplication and adding more test cases.

## Set of changes

* Remove wrapping args in reactive in useQuery.ts and useSubscription.ts
* Add `createRequestWithArgs()`, `useClientState()` and `useRequestState()` utils to avoid code duplication.
* Refactor `useQuery()`, `useSubscription()` and `useMutation()` to use shared  utils mentioned above.
* Add `isReadonly()` check before changing `isPaused` variable, as it could be readonly.
* ~~Remove `onBeforeUnmount` and unwatch handlers, as [watcher automatically stopped when the owner component is unmounted](https://vuejs.org/guide/essentials/watchers.html#stopping-a-watcher)~~
* Remove `flush: 'pre'` in watcher options, as this is default setting
* Rename `unref()` to `unwrap()` to avoid confusions with vue function
* Reuse `then()` handler for `useQuery()` for cleaner code and avoid duplications
* Cover of reactive arguments usage with test cases

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 3398b9f8edbcc1a899033d02077c14d9aa056703

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name      | Type  |
| --------- | ----- |
| @urql/vue | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/yurks/urql/new/feat/rewrite?filename=.changeset/cyan-apricots-agree.md&value=---%0A%22%40urql%2Fvue%22%3A%20patch%0A---%0A%0Afeat%3A%20refactor%20composable%20functions%0A)


negezor: This is a really good refactoring. The only thing that bothers me is why the effects in `useClientHandle()` were initially manually stopped; I couldn’t find the reason (or I didn’t look well).
yurks: Thanks @negezor ☀️

> why the effects in `useClientHandle()` were initially manually stopped

The reason of having `useClientHandle()` and manually stopping watchers I found is here https://github.com/urql-graphql/urql/pull/3610#issuecomment-2178843530. And the reason it was removed in current PR - I consider this as excessive concern for rare use cases and attempt to bypass vue limitations, which should be completely on users responsibility imho.

But I can agree this kind of removal is overkill for mine "little refactoring", so I'll revert this functionality here.
I guess we can found an optimal solution in scope of [that PR](https://github.com/urql-graphql/urql/pull/3610).



Patch:
diff --git a/.changeset/chatty-mice-join.md b/.changeset/chatty-mice-join.md
new file mode 100644
index 00000000..167b763f
--- /dev/null
+++ b/.changeset/chatty-mice-join.md
@@ -0,0 +1,5 @@
+---
+'@urql/vue': minor
+---
+
+Refactor composable functions with a focus on avoiding memory leaks and Vue best practices
diff --git a/docs/architecture.md b/docs/architecture.md
index b90d6941..a237db59 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -10,7 +10,7 @@ As you use it in your app, it's split into three parts:
 
 - Bindings — such as for React, Preact, Vue, or Svelte — which interact with `@urql/core`'s
   `Client`.
-- The Client — as created [with the core `@urql/core` package](./basics/code.md), which interacts with "exchanges" to execute GraphQL
+- The Client — as created [with the core `@urql/core` package](./basics/core.md), which interacts with "exchanges" to execute GraphQL
   operations, and which you can also use directly.
 - Exchanges, which provide functionality like fetching or caching to the `Client`.
 
diff --git a/examples/with-defer-stream-directives/package.json b/examples/with-defer-stream-directives/package.json
index 24361663..8bce4981 100644
--- a/examples/with-defer-stream-directives/package.json
+++ b/examples/with-defer-stream-directives/package.json
@@ -18,7 +18,7 @@
   "dependencies": {
     "@graphql-yoga/plugin-defer-stream": "^1.7.1",
     "@urql/core": "^5.0.4",
-    "@urql/exchange-graphcache": "^7.1.1",
+    "@urql/exchange-graphcache": "^7.1.2",
     "graphql": "17.0.0-alpha.2",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/with-graphcache-pagination/package.json b/examples/with-graphcache-pagination/package.json
index 7298a1ad..e507a127 100644
--- a/examples/with-graphcache-pagination/package.json
+++ b/examples/with-graphcache-pagination/package.json
@@ -7,7 +7,7 @@
   },
   "dependencies": {
     "@urql/core": "^5.0.4",
-    "@urql/exchange-graphcache": "^7.1.1",
+    "@urql/exchange-graphcache": "^7.1.2",
     "graphql": "^16.6.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/with-graphcache-updates/package.json b/examples/with-graphcache-updates/package.json
index 81089d76..e0fd8063 100644
--- a/examples/with-graphcache-updates/package.json
+++ b/examples/with-graphcache-updates/package.json
@@ -8,7 +8,7 @@
   "dependencies": {
     "@urql/core": "^5.0.4",
     "@urql/exchange-auth": "^2.2.0",
-    "@urql/exchange-graphcache": "^7.1.1",
+    "@urql/exchange-graphcache": "^7.1.2",
     "graphql": "^16.6.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/with-subscriptions-via-fetch/package.json b/examples/with-subscriptions-via-fetch/package.json
index e53dede0..c53d4780 100644
--- a/examples/with-subscriptions-via-fetch/package.json
+++ b/examples/with-subscriptions-via-fetch/package.json
@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@urql/core": "^5.0.4",
-    "@urql/exchange-graphcache": "^7.1.1",
+    "@urql/exchange-graphcache": "^7.1.2",
     "graphql": "^16.6.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/with-svelte/package.json b/examples/with-svelte/package.json
index 5c48c9b8..4f30ed19 100644
--- a/examples/with-svelte/package.json
+++ b/examples/with-svelte/package.json
@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@urql/core": "^5.0.4",
-    "@urql/svelte": "^4.2.0",
+    "@urql/svelte": "^4.2.1",
     "graphql": "^16.6.0",
     "svelte": "^4.0.5"
   },
diff --git a/exchanges/graphcache/CHANGELOG.md b/exchanges/graphcache/CHANGELOG.md
index b149eada..86b2983b 100644
--- a/exchanges/graphcache/CHANGELOG.md
+++ b/exchanges/graphcache/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @urql/exchange-graphcache
 
+## 7.1.2
+
+### Patch Changes
+
+- Disregard write-only operation when fragment-matching with schema awareness
+  Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3621](https://github.com/urql-graphql/urql/pull/3621))
+
 ## 7.1.1
 
 ### Patch Changes
diff --git a/exchanges/graphcache/jsr.json b/exchanges/graphcache/jsr.json
index 4949dfa6..16120bba 100644
--- a/exchanges/graphcache/jsr.json
+++ b/exchanges/graphcache/jsr.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/exchange-graphcache",
-  "version": "7.1.1",
+  "version": "7.1.2",
   "exports": {
     ".": "./src/index.ts",
     "./extras": "./src/extras/index.ts",
diff --git a/exchanges/graphcache/package.json b/exchanges/graphcache/package.json
index 2282cfe9..5fc70afd 100644
--- a/exchanges/graphcache/package.json
+++ b/exchanges/graphcache/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/exchange-graphcache",
-  "version": "7.1.1",
+  "version": "7.1.2",
   "description": "A normalized and configurable cache exchange for urql",
   "sideEffects": false,
   "homepage": "https://formidable.com/open-source/urql/docs/graphcache",
diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts
index 4dad5e09..bb1893a3 100644
--- a/exchanges/graphcache/src/operations/shared.ts
+++ b/exchanges/graphcache/src/operations/shared.ts
@@ -236,7 +236,10 @@ export function makeSelectionIterator(
                     ctx.store.logger
                   ));
 
-            if (isMatching || currentOperation === 'write') {
+            if (
+              isMatching ||
+              (currentOperation === 'write' && !ctx.store.schema)
+            ) {
               if (process.env.NODE_ENV !== 'production')
                 pushDebugNode(typename, fragment);
               const isFragmentOptional = isOptional(select);
diff --git a/packages/svelte-urql/CHANGELOG.md b/packages/svelte-urql/CHANGELOG.md
index 9946f28b..fef987e7 100644
--- a/packages/svelte-urql/CHANGELOG.md
+++ b/packages/svelte-urql/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @urql/svelte
 
+## 4.2.1
+
+### Patch Changes
+
+- add support for Svelte 5 in the `peerDependencies`
+  Submitted by [@itssumitrai](https://github.com/itssumitrai) (See [#3634](https://github.com/urql-graphql/urql/pull/3634))
+
 ## 4.2.0
 
 ### Minor Changes
diff --git a/packages/svelte-urql/jsr.json b/packages/svelte-urql/jsr.json
index ac8bb4a7..25fe911c 100644
--- a/packages/svelte-urql/jsr.json
+++ b/packages/svelte-urql/jsr.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/svelte",
-  "version": "4.2.0",
+  "version": "4.2.1",
   "exports": {
     ".": "./src/index.ts"
   },
diff --git a/packages/svelte-urql/package.json b/packages/svelte-urql/package.json
index 0a934ddd..bdb5b4e8 100644
--- a/packages/svelte-urql/package.json
+++ b/packages/svelte-urql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/svelte",
-  "version": "4.2.0",
+  "version": "4.2.1",
   "description": "A highly customizable and versatile GraphQL client for Svelte",
   "sideEffects": false,
   "homepage": "https://formidable.com/open-source/urql/docs/",
@@ -50,7 +50,7 @@
   },
   "peerDependencies": {
     "@urql/core": "^5.0.0",
-    "svelte": "^3.0.0 || ^4.0.0"
+    "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
   },
   "dependencies": {
     "@urql/core": "^5.0.0",
diff --git a/packages/vue-urql/src/useClientHandle.ts b/packages/vue-urql/src/useClientHandle.ts
index 962f2bed..60988d82 100644
--- a/packages/vue-urql/src/useClientHandle.ts
+++ b/packages/vue-urql/src/useClientHandle.ts
@@ -1,5 +1,4 @@
-import type { DocumentNode } from 'graphql';
-import type { AnyVariables, Client, TypedDocumentNode } from '@urql/core';
+import type { AnyVariables, Client, DocumentInput } from '@urql/core';
 import type { WatchStopHandle } from 'vue';
 import { getCurrentInstance, onMounted, onBeforeUnmount } from 'vue';
 
@@ -75,7 +74,7 @@ export interface ClientHandle {
    * function or when chained in an `async setup()` function.
    */
   useMutation<T = any, V extends AnyVariables = AnyVariables>(
-    query: TypedDocumentNode<T, V> | DocumentNode | string
+    query: DocumentInput<T, V>
   ): UseMutationResponse<T, V>;
 }
 
@@ -153,7 +152,7 @@ export function useClientHandle(): ClientHandle {
     },
 
     useMutation<T = any, V extends AnyVariables = AnyVariables>(
-      query: TypedDocumentNode<T, V> | DocumentNode | string
+      query: DocumentInput<T, V>
     ): UseMutationResponse<T, V> {
       return callUseMutation(query, client);
     },
diff --git a/packages/vue-urql/src/useMutation.ts b/packages/vue-urql/src/useMutation.ts
index e220ddc9..74fd65e9 100644
--- a/packages/vue-urql/src/useMutation.ts
+++ b/packages/vue-urql/src/useMutation.ts
@@ -1,24 +1,22 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 
 import type { Ref } from 'vue';
-import { ref, shallowRef } from 'vue';
-import type { DocumentNode } from 'graphql';
+import { ref } from 'vue';
 import { pipe, onPush, filter, toPromise, take } from 'wonka';
 
 import type {
   Client,
   AnyVariables,
-  TypedDocumentNode,
   CombinedError,
   Operation,
   OperationContext,
   OperationResult,
+  DocumentInput,
 } from '@urql/core';
-import { createRequest } from '@urql/core';
 
 import { useClient } from './useClient';
 import type { MaybeRef } from './utils';
-import { unref } from './utils';
+import { createRequestWithArgs, useRequestState } from './utils';
 
 /** State of the last mutation executed by {@link useMutation}.
  *
@@ -126,21 +124,21 @@ export interface UseMutationResponse<T, V extends AnyVariables = AnyVariables> {
  * ```
  */
 export function useMutation<T = any, V extends AnyVariables = AnyVariables>(
-  query: TypedDocumentNode<T, V> | DocumentNode | string
+  query: DocumentInput<T, V>
 ): UseMutationResponse<T, V> {
   return callUseMutation(query);
 }
 
 export function callUseMutation<T = any, V extends AnyVariables = AnyVariables>(
-  query: MaybeRef<TypedDocumentNode<T, V> | DocumentNode | string>,
+  query: MaybeRef<DocumentInput<T, V>>,
   client: Ref<Client> = useClient()
 ): UseMutationResponse<T, V> {
   const data: Ref<T | undefined> = ref();
-  const stale: Ref<boolean> = ref(false);
-  const fetching: Ref<boolean> = ref(false);
-  const error: Ref<CombinedError | undefined> = shallowRef();
-  const operation: Ref<Operation<T, V> | undefined> = shallowRef();
-  const extensions: Ref<Record<string, any> | undefined> = shallowRef();
+
+  const { fetching, operation, extensions, stale, error } = useRequestState<
+    T,
+    V
+  >();
 
   return {
     data,
@@ -157,7 +155,7 @@ export function callUseMutation<T = any, V extends AnyVariables = AnyVariables>(
 
       return pipe(
         client.value.executeMutation<T, V>(
-          createRequest<T, V>(unref(query), unref(variables)),
+          createRequestWithArgs({ query, variables }),
           context || {}
         ),
         onPush(result => {
diff --git a/packages/vue-urql/src/useQuery.ts b/packages/vue-urql/src/useQuery.ts
index 102edf43..b06fde3e 100644
--- a/packages/vue-urql/src/useQuery.ts
+++ b/packages/vue-urql/src/useQuery.ts
@@ -1,27 +1,25 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 
 import type { Ref, WatchStopHandle } from 'vue';
-import { isRef, ref, shallowRef, watch, watchEffect, reactive } from 'vue';
+import { ref, watchEffect } from 'vue';
 
-import type { Subscription, Source } from 'wonka';
+import type { Subscription } from 'wonka';
 import { pipe, subscribe, onEnd } from 'wonka';
 
 import type {
   Client,
   AnyVariables,
-  OperationResult,
   GraphQLRequestParams,
   CombinedError,
   OperationContext,
   RequestPolicy,
   Operation,
 } from '@urql/core';
-import { createRequest } from '@urql/core';
 
 import { useClient } from './useClient';
 
 import type { MaybeRef, MaybeRefObj } from './utils';
-import { unref, updateShallowRef } from './utils';
+import { useRequestState, useClientState } from './utils';
 
 /** Input arguments for the {@link useQuery} function.
  *
@@ -195,10 +193,6 @@ export type UseQueryResponse<
   V extends AnyVariables = AnyVariables,
 > = UseQueryState<T, V> & PromiseLike<UseQueryState<T, V>>;
 
-const watchOptions = {
-  flush: 'pre' as const,
-};
-
 /** Function to run a GraphQL query and get reactive GraphQL results.
  *
  * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options.
@@ -241,55 +235,83 @@ export function useQuery<T = any, V extends AnyVariables = AnyVariables>(
 }
 
 export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>(
-  _args: UseQueryArgs<T, V>,
+  args: UseQueryArgs<T, V>,
   client: Ref<Client> = useClient(),
-  stops: WatchStopHandle[] = []
+  stops?: WatchStopHandle[]
 ): UseQueryResponse<T, V> {
-  const args = reactive(_args) as UseQueryArgs<T, V>;
-
   const data: Ref<T | undefined> = ref();
-  const stale: Ref<boolean> = ref(false);
-  const fetching: Ref<boolean> = ref(false);
-  const error: Ref<CombinedError | undefined> = shallowRef();
-  const operation: Ref<Operation<T, V> | undefined> = shallowRef();
-  const extensions: Ref<Record<string, any> | undefined> = shallowRef();
 
-  const isPaused: Ref<boolean> = ref(!!unref(args.pause));
-  if (isRef(args.pause) || typeof args.pause === 'function') {
-    stops.push(watch(args.pause, value => (isPaused.value = value)));
-  }
+  const { fetching, operation, extensions, stale, error } = useRequestState<
+    T,
+    V
+  >();
 
-  const input = shallowRef({
-    request: createRequest<T, V>(unref(args.query), unref(args.variables) as V),
-    requestPolicy: unref(args.requestPolicy),
-    isPaused: isPaused.value,
-  });
+  const { isPaused, source, pause, resume, execute, teardown } = useClientState(
+    args,
+    client,
+    'executeQuery'
+  );
 
-  const source: Ref<Source<OperationResult<T, V>> | undefined> = ref();
+  const teardownQuery = watchEffect(
+    onInvalidate => {
+      if (source.value) {
+        fetching.value = true;
+        stale.value = false;
 
-  stops.push(
-    watchEffect(() => {
-      updateShallowRef(input, {
-        request: createRequest<T, V>(
-          unref(args.query),
-          unref(args.variables) as V
-        ),
-        requestPolicy: unref(args.requestPolicy),
-        isPaused: isPaused.value,
-      });
-    }, watchOptions)
+        onInvalidate(
+          pipe(
+            source.value,
+            onEnd(() => {
+              fetching.value = false;
+              stale.value = false;
+            }),
+            subscribe(res => {
+              data.value = res.data;
+              stale.value = !!res.stale;
+              fetching.value = false;
+              error.value = res.error;
+              operation.value = res.operation;
+              extensions.value = res.extensions;
+            })
+          ).unsubscribe
+        );
+      } else {
+        fetching.value = false;
+        stale.value = false;
+      }
+    },
+    {
+      // NOTE: This part of the query pipeline is only initialised once and will need
+      // to do so synchronously
+      flush: 'sync',
+    }
   );
 
-  stops.push(
-    watchEffect(() => {
-      source.value = !input.value.isPaused
-        ? client.value.executeQuery<T, V>(input.value.request, {
-            requestPolicy: unref(args.requestPolicy),
-            ...unref(args.context),
-          })
-        : undefined;
-    }, watchOptions)
-  );
+  stops && stops.push(teardown, teardownQuery);
+
+  const then: UseQueryResponse<T, V>['then'] = (onFulfilled, onRejected) => {
+    let sub: Subscription | void;
+
+    const promise = new Promise<UseQueryState<T, V>>(resolve => {
+      if (!source.value) {
+        return resolve(state);
+      }
+      let hasResult = false;
+      sub = pipe(
+        source.value,
+        subscribe(() => {
+          if (!state.fetching.value && !state.stale.value) {
+            if (sub) sub.unsubscribe();
+            hasResult = true;
+            resolve(state);
+          }
+        })
+      );
+      if (hasResult) sub.unsubscribe();
+    });
+
+    return promise.then(onFulfilled, onRejected);
+  };
 
   const state: UseQueryState<T, V> = {
     data,
@@ -299,105 +321,13 @@ export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>(
     extensions,
     fetching,
     isPaused,
+    pause,
+    resume,
     executeQuery(opts?: Partial<OperationContext>): UseQueryResponse<T, V> {
-      const s = (source.value = client.value.executeQuery<T, V>(
-        input.value.request,
-        {
-          requestPolicy: unref(args.requestPolicy),
-          ...unref(args.context),
-          ...opts,
-        }
-      ));
-
-      return {
-        ...response,
-        then(onFulfilled, onRejected) {
-          let sub: Subscription | void;
-          return new Promise<UseQueryState<T, V>>(resolve => {
-            let hasResult = false;
-            sub = pipe(
-              s,
-              subscribe(() => {
-                if (!state.fetching.value && !state.stale.value) {
-                  if (sub) sub.unsubscribe();
-                  hasResult = true;
-                  resolve(state);
-                }
-              })
-            );
-            if (hasResult) sub.unsubscribe();
-          }).then(onFulfilled, onRejected);
-        },
-      };
-    },
-    pause() {
-      isPaused.value = true;
-    },
-    resume() {
-      isPaused.value = false;
-    },
-  };
-
-  stops.push(
-    watchEffect(
-      onInvalidate => {
-        if (source.value) {
-          fetching.value = true;
-          stale.value = false;
-
-          onInvalidate(
-            pipe(
-              source.value,
-              onEnd(() => {
-                fetching.value = false;
-                stale.value = false;
-              }),
-              subscribe(res => {
-                data.value = res.data;
-                stale.value = !!res.stale;
-                fetching.value = false;
-                error.value = res.error;
-                operation.value = res.operation;
-                extensions.value = res.extensions;
-              })
-            ).unsubscribe
-          );
-        } else {
-          fetching.value = false;
-          stale.value = false;
-        }
-      },
-      {
-        // NOTE: This part of the query pipeline is only initialised once and will need
-        // to do so synchronously
-        flush: 'sync',
-      }
-    )
-  );
-
-  const response: UseQueryResponse<T, V> = {
-    ...state,
-    then(onFulfilled, onRejected) {
-      let sub: Subscription | void;
-      const promise = new Promise<UseQueryState<T, V>>(resolve => {
-        if (!source.value) return resolve(state);
-        let hasResult = false;
-        sub = pipe(
-          source.value,
-          subscribe(() => {
-            if (!state.fetching.value && !state.stale.value) {
-              if (sub) sub.unsubscribe();
-              hasResult = true;
-              resolve(state);
-            }
-          })
-        );
-        if (hasResult) sub.unsubscribe();
-      });
-
-      return promise.then(onFulfilled, onRejected);
+      execute(opts);
+      return { ...state, then };
     },
   };
 
-  return response;
+  return { ...state, then };
 }
diff --git a/packages/vue-urql/src/useSubscription.ts b/packages/vue-urql/src/useSubscription.ts
index c6d0545c..084d7586 100644
--- a/packages/vue-urql/src/useSubscription.ts
+++ b/packages/vue-urql/src/useSubscription.ts
@@ -1,26 +1,23 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 
-import type { Source } from 'wonka';
 import { pipe, subscribe, onEnd } from 'wonka';
 
 import type { Ref, WatchStopHandle } from 'vue';
-import { isRef, ref, shallowRef, watch, watchEffect, reactive } from 'vue';
+import { isRef, ref, watchEffect } from 'vue';
 
 import type {
   Client,
   GraphQLRequestParams,
   AnyVariables,
-  OperationResult,
   CombinedError,
   OperationContext,
   Operation,
 } from '@urql/core';
-import { createRequest } from '@urql/core';
 
 import { useClient } from './useClient';
 
 import type { MaybeRef, MaybeRefObj } from './utils';
-import { unref, updateShallowRef } from './utils';
+import { useRequestState, useClientState } from './utils';
 
 /** Input arguments for the {@link useSubscription} function.
  *
@@ -89,7 +86,9 @@ export type UseSubscriptionArgs<
 export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
 
 /** A {@link SubscriptionHandler} or a reactive ref of one. */
-export type SubscriptionHandlerArg<T, R> = MaybeRef<SubscriptionHandler<T, R>>;
+export type SubscriptionHandlerArg<T, R> =
+  | Ref<SubscriptionHandler<T, R>>
+  | SubscriptionHandler<T, R>;
 
 /** State of the current query, your {@link useSubscription} function is executing.
  *
@@ -182,10 +181,6 @@ export interface UseSubscriptionResponse<
   executeSubscription(opts?: Partial<OperationContext>): void;
 }
 
-const watchOptions = {
-  flush: 'pre' as const,
-};
-
 /** Function to run a GraphQL subscription and get reactive GraphQL results.
  *
  * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options.
@@ -229,7 +224,7 @@ export function useSubscription<
   V extends AnyVariables = AnyVariables,
 >(
   args: UseSubscriptionArgs<T, V>,
-  handler?: MaybeRef<SubscriptionHandler<T, R>>
+  handler?: SubscriptionHandlerArg<T, R>
 ): UseSubscriptionResponse<T, R, V> {
   return callUseSubscription(args, handler);
 }
@@ -239,86 +234,58 @@ export function callUseSubscription<
   R = T,
   V extends AnyVariables = AnyVariables,
 >(
-  _args: UseSubscriptionArgs<T, V>,
-  handler?: MaybeRef<SubscriptionHandler<T, R>>,
+  args: UseSubscriptionArgs<T, V>,
+  handler?: SubscriptionHandlerArg<T, R>,
   client: Ref<Client> = useClient(),
-  stops: WatchStopHandle[] = []
+  stops?: WatchStopHandle[]
 ): UseSubscriptionResponse<T, R, V> {
-  const args = reactive(_args) as UseSubscriptionArgs<T, V>;
-
   const data: Ref<R | undefined> = ref();
-  const stale: Ref<boolean> = ref(false);
-  const fetching: Ref<boolean> = ref(false);
-  const error: Ref<CombinedError | undefined> = shallowRef();
-  const operation: Ref<Operation<T, V> | undefined> = shallowRef();
-  const extensions: Ref<Record<string, any> | undefined> = shallowRef();
 
-  const scanHandler = ref(handler);
-  const isPaused: Ref<boolean> = ref(!!unref(args.pause));
-  if (isRef(args.pause) || typeof args.pause === 'function') {
-    stops.push(watch(args.pause, value => (isPaused.value = value)));
-  }
+  const { fetching, operation, extensions, stale, error } = useRequestState<
+    T,
+    V
+  >();
 
-  const input = shallowRef({
-    request: createRequest<T, V>(unref(args.query), unref(args.variables) as V),
-    isPaused: isPaused.value,
-  });
+  const { isPaused, source, pause, resume, execute, teardown } = useClientState(
+    args,
+    client,
+    'executeSubscription'
+  );
 
-  const source: Ref<Source<OperationResult<T, V>> | undefined> = ref();
+  const teardownSubscription = watchEffect(onInvalidate => {
+    if (source.value) {
+      fetching.value = true;
 
-  stops.push(
-    watchEffect(() => {
-      updateShallowRef(input, {
-        request: createRequest<T, V>(
-          unref(args.query),
-          unref(args.variables) as V
-        ),
-        isPaused: isPaused.value,
-      });
-    }, watchOptions)
-  );
+      onInvalidate(
+        pipe(
+          source.value,
+          onEnd(() => {
+            fetching.value = false;
+          }),
+          subscribe(result => {
+            fetching.value = true;
+            error.value = result.error;
+            extensions.value = result.extensions;
+            stale.value = !!result.stale;
+            operation.value = result.operation;
 
-  stops.push(
-    watchEffect(() => {
-      source.value = !isPaused.value
-        ? client.value.executeSubscription<T, V>(input.value.request, {
-            ...unref(args.context),
+            if (result.data != null && handler) {
+              const cb = isRef(handler) ? handler.value : handler;
+              if (typeof cb === 'function') {
+                data.value = cb(data.value, result.data);
+                return;
+              }
+            }
+            data.value = result.data as R;
           })
-        : undefined;
-    }, watchOptions)
-  );
-
-  stops.push(
-    watchEffect(onInvalidate => {
-      if (source.value) {
-        fetching.value = true;
+        ).unsubscribe
+      );
+    } else {
+      fetching.value = false;
+    }
+  });
 
-        onInvalidate(
-          pipe(
-            source.value,
-            onEnd(() => {
-              fetching.value = false;
-            }),
-            subscribe(result => {
-              fetching.value = true;
-              data.value =
-                result.data != null
-                  ? typeof scanHandler.value === 'function'
-                    ? scanHandler.value(data.value as any, result.data)
-                    : result.data
-                  : (result.data as any);
-              error.value = result.error;
-              extensions.value = result.extensions;
-              stale.value = !!result.stale;
-              operation.value = result.operation;
-            })
-          ).unsubscribe
-        );
-      } else {
-        fetching.value = false;
-      }
-    }, watchOptions)
-  );
+  stops && stops.push(teardown, teardownSubscription);
 
   const state: UseSubscriptionResponse<T, R, V> = {
     data,
@@ -328,25 +295,14 @@ export function callUseSubscription<
     extensions,
     fetching,
     isPaused,
+    pause,
+    resume,
     executeSubscription(
       opts?: Partial<OperationContext>
     ): UseSubscriptionResponse<T, R, V> {
-      source.value = client.value.executeSubscription<T, V>(
-        input.value.request,
-        {
-          ...unref(args.context),
-          ...opts,
-        }
-      );
-
+      execute(opts);
       return state;
     },
-    pause() {
-      isPaused.value = true;
-    },
-    resume() {
-      isPaused.value = false;
-    },
   };
 
   return state;
diff --git a/packages/vue-urql/src/utils.ts b/packages/vue-urql/src/utils.ts
index a9a83cda..a7806bdd 100644
--- a/packages/vue-urql/src/utils.ts
+++ b/packages/vue-urql/src/utils.ts
@@ -1,45 +1,171 @@
-import type { GraphQLRequest, AnyVariables } from '@urql/core';
-import type { Ref, ShallowRef } from 'vue';
-import { isRef } from 'vue';
+import type {
+  AnyVariables,
+  Client,
+  CombinedError,
+  DocumentInput,
+  Operation,
+  OperationContext,
+  OperationResult,
+  OperationResultSource,
+} from '@urql/core';
+import { createRequest } from '@urql/core';
+import type { Ref } from 'vue';
+import { watchEffect, isReadonly, computed, ref, shallowRef, isRef } from 'vue';
+import type { UseSubscriptionArgs } from './useSubscription';
+import type { UseQueryArgs } from './useQuery';
 
 export type MaybeRef<T> = T | (() => T) | Ref<T>;
 export type MaybeRefObj<T> = T extends {}
   ? { [K in keyof T]: MaybeRef<T[K]> }
   : T;
 
-export const unref = <T>(maybeRef: MaybeRef<T>): T =>
+const unwrap = <T>(maybeRef: MaybeRef<T>): T =>
   typeof maybeRef === 'function'
     ? (maybeRef as () => T)()
     : maybeRef != null && isRef(maybeRef)
     ? maybeRef.value
     : maybeRef;
 
-export interface RequestState<
-  Data = any,
-  Variables extends AnyVariables = AnyVariables,
-> {
-  request: GraphQLRequest<Data, Variables>;
-  isPaused: boolean;
-}
+const isPlainObject = (value: any): boolean => {
+  if (typeof value !== 'object' || value === null) return false;
+  return (
+    value.constructor &&
+    Object.getPrototypeOf(value).constructor === Object.prototype.constructor
+  );
+};
+export const isArray = Array.isArray;
 
-export function createRequestState<
-  Data = any,
-  Variables extends AnyVariables = AnyVariables,
->(
-  request: GraphQLRequest<Data, Variables>,
-  isPaused: boolean
-): RequestState<Data, Variables> {
-  return { request, isPaused };
-}
+const unwrapDeeply = <T>(input: T): T => {
+  input = isRef(input) ? (input.value as T) : input;
 
-export const updateShallowRef = <T extends Record<string, any>>(
-  ref: ShallowRef<T>,
-  next: T
-) => {
-  for (const key in next) {
-    if (ref.value[key] !== next[key]) {
-      ref.value = next;
-      return;
+  if (typeof input === 'function') {
+    return unwrapDeeply(input()) as T;
+  }
+
+  if (input && typeof input === 'object') {
+    if (isArray(input)) {
+      const length = input.length;
+      const out = new Array(length) as T;
+      let i = 0;
+      for (; i < length; i++) {
+        out[i] = unwrapDeeply(input[i]);
+      }
+
+      return out;
+    } else if (isPlainObject(input)) {
+      const keys = Object.keys(input);
+      const length = keys.length;
+      let i = 0;
+      let key: string;
+      const out = {} as T;
+
+      for (; i < length; i++) {
+        key = keys[i];
+        out[key] = unwrapDeeply(input[key]);
+      }
+
+      return out;
     }
   }
+
+  return input;
+};
+
+export const createRequestWithArgs = <
+  T = any,
+  V extends AnyVariables = AnyVariables,
+>(
+  args:
+    | UseQueryArgs<T, V>
+    | UseSubscriptionArgs<T, V>
+    | { query: MaybeRef<DocumentInput<T, V>>; variables: V }
+) => {
+  return createRequest<T, V>(
+    unwrap(args.query),
+    unwrapDeeply(args.variables) as V
+  );
 };
+
+export const useRequestState = <
+  T = any,
+  V extends AnyVariables = AnyVariables,
+>() => {
+  const stale: Ref<boolean> = ref(false);
+  const fetching: Ref<boolean> = ref(false);
+  const error: Ref<CombinedError | undefined> = shallowRef();
+  const operation: Ref<Operation<T, V> | undefined> = shallowRef();
+  const extensions: Ref<Record<string, any> | undefined> = shallowRef();
+  return {
+    stale,
+    fetching,
+    error,
+    operation,
+    extensions,
+  };
+};
+
+export function useClientState<T = any, V extends AnyVariables = AnyVariables>(
+  args: UseQueryArgs<T, V> | UseSubscriptionArgs<T, V>,
+  client: Ref<Client>,
+  method: keyof Pick<Client, 'executeSubscription' | 'executeQuery'>
+) {
+  const source: Ref<OperationResultSource<OperationResult<T, V>> | undefined> =
+    shallowRef();
+
+  const isPaused: Ref<boolean> = isRef(args.pause)
+    ? args.pause
+    : typeof args.pause === 'function'
+    ? computed(args.pause)
+    : ref(!!args.pause);
+
+  const request = computed(() => createRequestWithArgs(args));
+
+  const requestOptions = computed(() => {
+    return 'requestPolicy' in args
+      ? {
+          requestPolicy: unwrap(args.requestPolicy),
+          ...unwrap(args.context),
+        }
+      : {
+          ...unwrap(args.context),
+        };
+  });
+
+  const pause = () => {
+    if (!isReadonly(isPaused)) {
+      isPaused.value = true;
+    }
+  };
+
+  const resume = () => {
+    if (!isReadonly(isPaused)) {
+      isPaused.value = false;
+    }
+  };
+
+  const executeRaw = (opts?: Partial<OperationContext>) => {
+    return client.value[method]<T, V>(request.value, {
+      ...requestOptions.value,
+      ...opts,
+    });
+  };
+
+  const execute = (opts?: Partial<OperationContext>) => {
+    source.value = executeRaw(opts);
+  };
+
+  // it's important to use `watchEffect()` here instead of `watch()`
+  // because it listening for reactive variables inside `executeRaw()` function
+  const teardown = watchEffect(() => {
+    source.value = !isPaused.value ? executeRaw() : undefined;
+  });
+
+  return {
+    source,
+    isPaused,
+    pause,
+    resume,
+    execute,
+    teardown,
+  };
+}


Test patch:
diff --git a/packages/vue-urql/src/useMutation.test.ts b/packages/vue-urql/src/useMutation.test.ts
index 9d4e8ea0..50b79922 100644
--- a/packages/vue-urql/src/useMutation.test.ts
+++ b/packages/vue-urql/src/useMutation.test.ts
@@ -1,5 +1,5 @@
 import { OperationResult, OperationResultSource } from '@urql/core';
-import { reactive } from 'vue';
+import { readonly } from 'vue';
 import { vi, expect, it, beforeEach, describe } from 'vitest';
 
 vi.mock('./useClient.ts', async () => {
@@ -30,15 +30,13 @@ describe('useMutation', () => {
         () => subject.source as OperationResultSource<OperationResult>
       );
 
-    const mutation = reactive(
-      useMutation(gql`
-        mutation {
-          test
-        }
-      `)
-    );
+    const mutation = useMutation(gql`
+      mutation {
+        test
+      }
+    `);
 
-    expect(mutation).toMatchObject({
+    expect(readonly(mutation)).toMatchObject({
       data: undefined,
       stale: false,
       fetching: false,
@@ -50,18 +48,18 @@ describe('useMutation', () => {
 
     const promise = mutation.executeMutation({ test: true });
 
-    expect(mutation.fetching).toBe(true);
-    expect(mutation.stale).toBe(false);
-    expect(mutation.error).toBe(undefined);
+    expect(mutation.fetching.value).toBe(true);
+    expect(mutation.stale.value).toBe(false);
+    expect(mutation.error.value).toBe(undefined);
 
     expect(clientMutation).toHaveBeenCalledTimes(1);
 
     subject.next({ data: { test: true }, stale: false });
-    await promise.then(function () {
-      expect(mutation.fetching).toBe(false);
-      expect(mutation.stale).toBe(false);
-      expect(mutation.error).toBe(undefined);
-      expect(mutation.data).toEqual({ test: true });
-    });
+
+    await promise;
+    expect(mutation.fetching.value).toBe(false);
+    expect(mutation.stale.value).toBe(false);
+    expect(mutation.error.value).toBe(undefined);
+    expect(mutation.data.value).toHaveProperty('test', true);
   });
 });
diff --git a/packages/vue-urql/src/useQuery.test.ts b/packages/vue-urql/src/useQuery.test.ts
index 62abb2d9..a28bf0d6 100644
--- a/packages/vue-urql/src/useQuery.test.ts
+++ b/packages/vue-urql/src/useQuery.test.ts
@@ -1,5 +1,9 @@
-import { OperationResult, OperationResultSource } from '@urql/core';
-import { nextTick, reactive, ref } from 'vue';
+import {
+  OperationResult,
+  OperationResultSource,
+  RequestPolicy,
+} from '@urql/core';
+import { computed, nextTick, reactive, readonly, ref } from 'vue';
 import { vi, expect, it, describe } from 'vitest';
 
 vi.mock('./useClient.ts', async () => ({
@@ -10,10 +14,28 @@ vi.mock('./useClient.ts', async () => ({
 
 import { pipe, makeSubject, fromValue, delay } from 'wonka';
 import { createClient } from '@urql/core';
-import { useQuery } from './useQuery';
+import { useQuery, UseQueryArgs } from './useQuery';
 
 const client = createClient({ url: '/graphql', exchanges: [] });
 
+const createQuery = (args: UseQueryArgs) => {
+  const executeQuery = vi
+    .spyOn(client, 'executeQuery')
+    .mockImplementation(request => {
+      return pipe(
+        fromValue({ operation: request, data: { test: true } }),
+        delay(1)
+      ) as any;
+    });
+
+  const query$ = useQuery(args);
+
+  return {
+    query$,
+    executeQuery,
+  };
+};
+
 describe('useQuery', () => {
   it('runs a query and updates data', async () => {
     const subject = makeSubject<any>();
@@ -23,12 +45,11 @@ describe('useQuery', () => {
         () => subject.source as OperationResultSource<OperationResult>
       );
 
-    const _query = useQuery({
+    const query = useQuery({
       query: `{ test }`,
     });
-    const query = reactive(_query);
 
-    expect(query).toMatchObject({
+    expect(readonly(query)).toMatchObject({
       data: undefined,
       stale: false,
       fetching: true,
@@ -54,12 +75,12 @@ describe('useQuery', () => {
       }
     );
 
-    expect(query.fetching).toBe(true);
+    expect(query.fetching.value).toBe(true);
 
     subject.next({ data: { test: true } });
 
-    expect(query.fetching).toBe(false);
-    expect(query.data).toEqual({ test: true });
+    expect(query.fetching.value).toBe(false);
+    expect(query.data.value).toHaveProperty('test', true);
   });
 
   it('runs queries as a promise-like that resolves when used', async () => {
@@ -79,18 +100,9 @@ describe('useQuery', () => {
   });
 
   it('runs queries as a promise-like that resolves even when the query changes', async () => {
-    const executeQuery = vi
-      .spyOn(client, 'executeQuery')
-      .mockImplementation(request => {
-        return pipe(
-          fromValue({ operation: request, data: { test: true } }),
-          delay(1)
-        ) as any;
-      });
-
     const doc = ref('{ test }');
 
-    const query$ = useQuery({
+    const { executeQuery, query$ } = createQuery({
       query: doc,
     });
 
@@ -108,36 +120,239 @@ describe('useQuery', () => {
     );
   });
 
-  it('reacts to variables changing', async () => {
-    const executeQuery = vi
-      .spyOn(client, 'executeQuery')
-      .mockImplementation(request => {
-        return pipe(
-          fromValue({ operation: request, data: { test: true } }),
-          delay(1)
-        ) as any;
-      });
+  it('runs a query with different variables', async () => {
+    const simpleVariables = {
+      null: null,
+      NaN: NaN,
+      empty: '',
+      bool: false,
+      int: 1,
+      float: 1.1,
+      string: 'string',
+      blob: new Blob(),
+      date: new Date(),
+    };
 
-    const variables = {
-      test: ref(1),
+    const variablesSet = {
+      func: () => 'func',
+      ref: ref('ref'),
+      computed: computed(() => 'computed'),
+      ...simpleVariables,
     };
-    const query$ = useQuery({
-      query: '{ test }',
+
+    const variablesSetUnwrapped = {
+      func: 'func',
+      ref: 'ref',
+      computed: 'computed',
+      ...simpleVariables,
+    };
+
+    const { query$ } = createQuery({
+      query: ref('{ test }'),
+      variables: {
+        ...variablesSet,
+        nested: variablesSet,
+        array: [variablesSet],
+      },
+    });
+
+    await query$;
+
+    expect(query$.operation.value?.variables).toStrictEqual({
+      ...variablesSetUnwrapped,
+      nested: variablesSetUnwrapped,
+      array: [variablesSetUnwrapped],
+    });
+  });
+
+  it('reacts to ref variables changing', async () => {
+    const variables = ref({ prop: 1 });
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
       variables,
     });
 
     await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 1);
+
+    variables.value.prop++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 2);
+
+    variables.value = { prop: 3 };
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(3);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 3);
+  });
 
+  it('reacts to nested ref variables changing', async () => {
+    const prop = ref(1);
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      variables: { prop },
+    });
+
+    await query$;
     expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 1);
+
+    prop.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 2);
+  });
 
-    expect(query$.operation.value).toHaveProperty('variables.test', 1);
+  it('reacts to deep nested ref variables changing', async () => {
+    const prop = ref(1);
 
-    variables.test.value = 2;
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      variables: { deep: { nested: { prop } } },
+    });
 
     await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty(
+      'variables.deep.nested.prop',
+      1
+    );
 
+    prop.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty(
+      'variables.deep.nested.prop',
+      2
+    );
+  });
+
+  it('reacts to reactive variables changing', async () => {
+    const prop = ref(1);
+    const variables = reactive({ prop: 1, deep: { nested: { prop } } });
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      variables,
+    });
+
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 1);
+
+    variables.prop++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 2);
+
+    prop.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(3);
+    expect(query$.operation.value).toHaveProperty(
+      'variables.deep.nested.prop',
+      2
+    );
+  });
+
+  it('reacts to computed variables changing', async () => {
+    const prop = ref(1);
+    const prop2 = ref(1);
+    const variables = computed(() => ({
+      prop: prop.value,
+      deep: { nested: { prop2 } },
+    }));
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      variables,
+    });
+
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 1);
+
+    prop.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 2);
+
+    prop2.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(3);
+    expect(query$.operation.value).toHaveProperty(
+      'variables.deep.nested.prop2',
+      2
+    );
+  });
+
+  it('reacts to callback variables changing', async () => {
+    const prop = ref(1);
+    const prop2 = ref(1);
+    const variables = () => ({
+      prop: prop.value,
+      deep: { nested: { prop2 } },
+    });
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      variables,
+    });
+
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 1);
+
+    prop.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(query$.operation.value).toHaveProperty('variables.prop', 2);
+
+    prop2.value++;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(3);
+    expect(query$.operation.value).toHaveProperty(
+      'variables.deep.nested.prop2',
+      2
+    );
+  });
+
+  it('reacts to reactive context argument', async () => {
+    const context = ref<{ requestPolicy: RequestPolicy }>({
+      requestPolicy: 'cache-only',
+    });
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      context,
+    });
+
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+
+    context.value.requestPolicy = 'network-only';
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+  });
+
+  it('reacts to callback context argument', async () => {
+    const requestPolicy = ref<RequestPolicy>('cache-only');
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      context: () => ({
+        requestPolicy: requestPolicy.value,
+      }),
+    });
+
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+
+    requestPolicy.value = 'network-only';
+    await query$;
     expect(executeQuery).toHaveBeenCalledTimes(2);
-    expect(query$.operation.value).toHaveProperty('variables.test', 2);
   });
 
   it('pauses query when asked to do so', async () => {
@@ -148,23 +363,86 @@ describe('useQuery', () => {
         () => subject.source as OperationResultSource<OperationResult>
       );
 
-    const pause = ref(true);
-
-    const _query = useQuery({
+    const query = useQuery({
       query: `{ test }`,
-      pause: () => pause.value,
+      pause: true,
     });
-    const query = reactive(_query);
 
     expect(executeQuery).not.toHaveBeenCalled();
 
-    pause.value = false;
+    query.resume();
     await nextTick();
-    expect(query.fetching).toBe(true);
+    expect(query.fetching.value).toBe(true);
 
     subject.next({ data: { test: true } });
 
-    expect(query.fetching).toBe(false);
-    expect(query.data).toEqual({ test: true });
+    expect(query.fetching.value).toBe(false);
+    expect(query.data.value).toHaveProperty('test', true);
+  });
+
+  it('pauses query with ref variable', async () => {
+    const pause = ref(true);
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      pause,
+    });
+
+    await query$;
+    expect(executeQuery).not.toHaveBeenCalled();
+
+    pause.value = false;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+
+    query$.pause();
+    query$.resume();
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+  });
+
+  it('pauses query with computed variable', async () => {
+    const pause = ref(true);
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      pause: computed(() => pause.value),
+    });
+
+    await query$;
+    expect(executeQuery).not.toHaveBeenCalled();
+
+    pause.value = false;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+
+    query$.pause();
+    query$.resume();
+    await query$;
+    // this shouldn't be called, as pause/resume functionality should works in sync with passed `pause` variable, e.g.:
+    // if we pass readonly computed variable, then we want to make sure that its value fully controls the state of the request.
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+  });
+
+  it('pauses query with callback', async () => {
+    const pause = ref(true);
+
+    const { executeQuery, query$ } = createQuery({
+      query: ref('{ test }'),
+      pause: () => pause.value,
+    });
+
+    await query$;
+    expect(executeQuery).not.toHaveBeenCalled();
+
+    pause.value = false;
+    await query$;
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+
+    query$.pause();
+    query$.resume();
+    await query$;
+    // the same as computed variable example - user has full control over the request state if using callback
+    expect(executeQuery).toHaveBeenCalledTimes(1);
   });
 });
diff --git a/packages/vue-urql/src/useSubscription.test.ts b/packages/vue-urql/src/useSubscription.test.ts
index 67375b38..232e0ce3 100644
--- a/packages/vue-urql/src/useSubscription.test.ts
+++ b/packages/vue-urql/src/useSubscription.test.ts
@@ -1,7 +1,7 @@
 // @vitest-environment jsdom
 
 import { OperationResult, OperationResultSource } from '@urql/core';
-import { nextTick, reactive, ref } from 'vue';
+import { nextTick, readonly, ref } from 'vue';
 import { vi, expect, it, describe } from 'vitest';
 
 vi.mock('./useClient.ts', async () => ({
@@ -25,13 +25,11 @@ describe('useSubscription', () => {
         () => subject.source as OperationResultSource<OperationResult>
       );
 
-    const sub = reactive(
-      useSubscription({
-        query: `{ test }`,
-      })
-    );
+    const sub = useSubscription({
+      query: `{ test }`,
+    });
 
-    expect(sub).toMatchObject({
+    expect(readonly(sub)).toMatchObject({
       data: undefined,
       stale: false,
       fetching: true,
@@ -53,12 +51,13 @@ describe('useSubscription', () => {
       expect.any(Object)
     );
 
-    expect(sub.fetching).toBe(true);
+    expect(sub.fetching.value).toBe(true);
 
     subject.next({ data: { test: true } });
-    expect(sub.data).toEqual({ test: true });
+    expect(sub.data.value).toHaveProperty('test', true);
+
     subject.complete();
-    expect(sub.fetching).toBe(false);
+    expect(sub.fetching.value).toBe(false);
   });
 
   it('updates the executed subscription when inputs change', async () => {
@@ -70,12 +69,10 @@ describe('useSubscription', () => {
       );
 
     const variables = ref({});
-    const sub = reactive(
-      useSubscription({
-        query: `{ test }`,
-        variables,
-      })
-    );
+    const sub = useSubscription({
+      query: `{ test }`,
+      variables,
+    });
 
     expect(executeSubscription).toHaveBeenCalledWith(
       {
@@ -87,7 +84,7 @@ describe('useSubscription', () => {
     );
 
     subject.next({ data: { test: true } });
-    expect(sub.data).toEqual({ test: true });
+    expect(sub.data.value).toHaveProperty('test', true);
 
     variables.value = { test: true };
     await nextTick();
@@ -101,9 +98,10 @@ describe('useSubscription', () => {
       expect.any(Object)
     );
 
-    expect(sub.fetching).toBe(true);
-    expect(sub.data).toEqual({ test: true });
+    expect(sub.fetching.value).toBe(true);
+    expect(sub.data.value).toHaveProperty('test', true);
   });
+
   it('supports a custom scanning handler', async () => {
     const subject = makeSubject<any>();
     const executeSubscription = vi
@@ -115,13 +113,12 @@ describe('useSubscription', () => {
     const scanHandler = (currentState: any, nextState: any) => ({
       counter: (currentState ? currentState.counter : 0) + nextState.counter,
     });
-    const sub = reactive(
-      useSubscription(
-        {
-          query: `subscription { counter }`,
-        },
-        scanHandler
-      )
+
+    const sub = useSubscription(
+      {
+        query: `subscription { counter }`,
+      },
+      scanHandler
     );
 
     expect(executeSubscription).toHaveBeenCalledWith(
@@ -134,9 +131,9 @@ describe('useSubscription', () => {
     );
 
     subject.next({ data: { counter: 1 } });
-    expect(sub.data).toEqual({ counter: 1 });
+    expect(sub.data.value).toHaveProperty('counter', 1);
 
     subject.next({ data: { counter: 2 } });
-    expect(sub.data).toEqual({ counter: 3 });
+    expect(sub.data.value).toHaveProperty('counter', 3);
   });
 });