Repository: urql-graphql/urql
Pull request #3632 (2024-07-15 17:18:40)

perf(core): remove duplicate `JSON.stringify` call on `data` in `ssrExchange`

## Summary

Removes the unnecessary `JSON.stringify` when serializing, since it will be called below anyway

![image](https://github.com/user-attachments/assets/6ea2e313-3740-4a8e-945a-e7fc13d03558)

## Set of changes

Removing the `data` pre-property in exchange/ssr during serialization


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 719fa62377bda7fdcff9e3f4a354823eabd59989

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/negezor/urql/new/patch-1?filename=.changeset/tender-wolves-compare.md&value=---%0A%22%40urql%2Fcore%22%3A%20patch%0A---%0A%0Aperf(ssr)%3A%20remove%20double%20%60JSON.stringify()%60%20for%20%60data%60%20in%20%60serializeResult%60%0A)


kitten: Haven't looked at this closely yet, but just to point this out ahead of time, this may necessitate a major, or at least a minor bump. We can't be sure of how this is used and hence we can't guarantee that all apps that have been built ensure that the versions of serialisation are the same in the SSR process and on the client-side.

Going for backwards compatibility here would also work.
negezor: It doesn't require any breaking changes, as the result is identical, because we are just doing the same action twice. I have already tested in two applications at my place, one on Nuxt and one on custom SSR.

On the other hand, the changes worth introducing are custom serializers and deserializers. For example something like this:
```js
import { uneval } from 'devalue';
// ...

const ssr = ssrExchange({
    isClient: !import.meta.env.SSR,

    // By default
    // serialize: JSON.stringify,
    // deserialize: JSON.parse,

    // Custom for devalue
    serialize: data => data,
    deserialize: data => data,
});

// ...

if (import.meta.env.CLIENT) {
  ssr.restoreData(window.__GRAPHQL__ || {});
}

// ...

if (import.meta.env.SSR) {
  const graphqlState = uneval(ssr.extractData());
  return render('index.html', { graphqlState })
}
```

This will be backwards compatible for the reason that we will default to JSON methods

Patch:
diff --git a/.changeset/sweet-goats-judge.md b/.changeset/sweet-goats-judge.md
new file mode 100644
index 00000000..5f7c2fb2
--- /dev/null
+++ b/.changeset/sweet-goats-judge.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Removes double serialization of `data` in `ssrExchange`
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/examples/with-vue3/package.json b/examples/with-vue3/package.json
index 5e846eaa..a831700a 100644
--- a/examples/with-vue3/package.json
+++ b/examples/with-vue3/package.json
@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@urql/core": "^5.0.4",
-    "@urql/vue": "^1.3.2",
+    "@urql/vue": "^1.4.0",
     "graphql": "^16.6.0",
     "vue": "^3.2.47"
   },
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index 10fff64c..6716a54f 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -109,7 +109,6 @@ const serializeResult = (
   includeExtensions: boolean
 ): SerializedResult => {
   const serialized: SerializedResult = {
-    data: JSON.stringify(result.data),
     hasNext: result.hasNext,
   };
 
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/CHANGELOG.md b/packages/vue-urql/CHANGELOG.md
index a57292fe..1904c537 100644
--- a/packages/vue-urql/CHANGELOG.md
+++ b/packages/vue-urql/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @urql/vue
 
+## 1.4.0
+
+### Minor Changes
+
+- Refactor composable functions with a focus on avoiding memory leaks and Vue best practices
+  Submitted by [@yurks](https://github.com/yurks) (See [#3619](https://github.com/urql-graphql/urql/pull/3619))
+
 ## 1.3.2
 
 ### Patch Changes
diff --git a/packages/vue-urql/jsr.json b/packages/vue-urql/jsr.json
index df5aa933..7a77fe5e 100644
--- a/packages/vue-urql/jsr.json
+++ b/packages/vue-urql/jsr.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/vue",
-  "version": "1.3.2",
+  "version": "1.4.0",
   "exports": {
     ".": "./src/index.ts"
   },
diff --git a/packages/vue-urql/package.json b/packages/vue-urql/package.json
index 1f62a5a7..4ddf7856 100644
--- a/packages/vue-urql/package.json
+++ b/packages/vue-urql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@urql/vue",
-  "version": "1.3.2",
+  "version": "1.4.0",
   "description": "A highly customizable and versatile GraphQL client for vue",
   "sideEffects": false,
   "homepage": "https://formidable.com/open-source/urql/docs/",
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);
   });
 });