diff --git a/.changeset/vue-query-await-cleanup.md b/.changeset/vue-query-await-cleanup.md new file mode 100644 index 00000000..6156f957 --- /dev/null +++ b/.changeset/vue-query-await-cleanup.md @@ -0,0 +1,5 @@ +--- +'@urql/vue': patch +--- + +Fix `await useQuery()` in Vue to wait on the composable's existing reactive state instead of creating an additional query source subscription. diff --git a/packages/vue-urql/src/useQuery.test.ts b/packages/vue-urql/src/useQuery.test.ts index da68bec7..3cbf822f 100644 --- a/packages/vue-urql/src/useQuery.test.ts +++ b/packages/vue-urql/src/useQuery.test.ts @@ -99,6 +99,57 @@ describe('useQuery', () => { expect(query.data.value).toEqual({ test: true }); }); + it('does not subscribe to queries again when awaited', async () => { + const subject = makeSubject(); + let subscriptions = 0; + const source = ((sink: any) => { + subscriptions++; + return subject.source(sink); + }) as OperationResultSource; + + const executeQuery = vi + .spyOn(client, 'executeQuery') + .mockImplementation(() => source); + + const query = useQuery({ + query: `{ test }`, + }); + + expect(subscriptions).toBe(1); + + const promise = query.then(value => value); + + expect(subscriptions).toBe(1); + + subject.next({ data: { test: true } }); + + const result = await promise; + + expect(executeQuery).toHaveBeenCalledTimes(1); + expect(subscriptions).toBe(1); + expect(result.fetching.value).toBe(false); + expect(result.data.value).toEqual({ test: true }); + }); + + it('resolves as a promise-like after the query has already settled', async () => { + const subject = makeSubject(); + vi.spyOn(client, 'executeQuery').mockImplementation( + () => subject.source as OperationResultSource + ); + + const query = useQuery({ + query: `{ test }`, + }); + + subject.next({ data: { test: true } }); + + expect(query.fetching.value).toBe(false); + + const result = await query; + + expect(result.data.value).toEqual({ test: true }); + }); + it('runs queries as a promise-like that resolves even when the query changes', async () => { const doc = ref('{ test }'); diff --git a/packages/vue-urql/src/useQuery.ts b/packages/vue-urql/src/useQuery.ts index 967d5eab..b043baa5 100644 --- a/packages/vue-urql/src/useQuery.ts +++ b/packages/vue-urql/src/useQuery.ts @@ -1,9 +1,8 @@ /* eslint-disable react-hooks/rules-of-hooks */ import type { Ref, WatchStopHandle } from 'vue'; -import { shallowRef, watchEffect } from 'vue'; +import { shallowRef, watch, watchEffect } from 'vue'; -import type { Subscription } from 'wonka'; import { pipe, subscribe, onEnd } from 'wonka'; import type { @@ -292,8 +291,6 @@ export function callUseQuery( stops && stops.push(teardown, teardownQuery); const then: UseQueryResponse['then'] = (onFulfilled, onRejected) => { - let sub: Subscription | void; - const promise = new Promise>(resolve => { // If there's no source (e.g. the query is paused) or we already hold a // settled result — for instance one that the `ssrExchange` replayed @@ -305,18 +302,17 @@ export function callUseQuery( if (!source.value || (!fetching.value && !stale.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; + + const stop = watch( + [fetching, stale], + ([isFetching, isStale]) => { + if (!isFetching && !isStale) { + stop(); resolve(state); } - }) + }, + { flush: 'sync' } ); - if (hasResult) sub.unsubscribe(); }); return promise.then(onFulfilled, onRejected);