From 4042579327f8b4b55d8cd6f64d3b233b834d942e Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Wed, 26 Nov 2025 15:50:22 +0100 Subject: [PATCH] feat(vue-query): allow options getters in additional composables --- .changeset/curvy-webs-create.md | 5 ++ .../vue-query/src/__mocks__/useBaseQuery.ts | 6 +- .../src/__tests__/useInfiniteQuery.test.ts | 2 +- .../src/__tests__/useIsFetching.test.ts | 32 ++++++--- .../src/__tests__/useIsMutating.test.ts | 56 ++++++++++++++- .../src/__tests__/useMutation.test.ts | 33 +++++++++ .../src/__tests__/useQueries.test.ts | 69 ++++++++++++++++++- .../vue-query/src/__tests__/useQuery.test.ts | 41 +++++++++-- packages/vue-query/src/useIsFetching.ts | 11 ++- packages/vue-query/src/useMutation.ts | 23 +++++-- packages/vue-query/src/useMutationState.ts | 24 ++++--- packages/vue-query/src/useQueries.ts | 7 +- 12 files changed, 270 insertions(+), 39 deletions(-) create mode 100644 .changeset/curvy-webs-create.md diff --git a/.changeset/curvy-webs-create.md b/.changeset/curvy-webs-create.md new file mode 100644 index 0000000000..9c79f2be0c --- /dev/null +++ b/.changeset/curvy-webs-create.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': minor +--- + +feat(vue-query): allow options getters in additional composables diff --git a/packages/vue-query/src/__mocks__/useBaseQuery.ts b/packages/vue-query/src/__mocks__/useBaseQuery.ts index 6388057709..1166eff1d0 100644 --- a/packages/vue-query/src/__mocks__/useBaseQuery.ts +++ b/packages/vue-query/src/__mocks__/useBaseQuery.ts @@ -1,9 +1,9 @@ import { vi } from 'vitest' import type { Mock } from 'vitest' -const { useBaseQuery: originImpl, unrefQueryArgs: originalParse } = - (await vi.importActual('../useBaseQuery')) as any +const { useBaseQuery: originImpl } = (await vi.importActual( + '../useBaseQuery', +)) as any export const useBaseQuery: Mock<(...args: Array) => any> = vi.fn(originImpl) -export const unrefQueryArgs = originalParse diff --git a/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts index d78eb3ad07..b9eee7547f 100644 --- a/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts +++ b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts @@ -5,7 +5,7 @@ import { infiniteQueryOptions } from '../infiniteQueryOptions' vi.mock('../useQueryClient') -describe('useQuery', () => { +describe('useInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) diff --git a/packages/vue-query/src/__tests__/useIsFetching.test.ts b/packages/vue-query/src/__tests__/useIsFetching.test.ts index 805f002404..50199934f5 100644 --- a/packages/vue-query/src/__tests__/useIsFetching.test.ts +++ b/packages/vue-query/src/__tests__/useIsFetching.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { onScopeDispose, reactive } from 'vue-demi' +import { onScopeDispose, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useIsFetching } from '../useIsFetching' @@ -65,15 +65,10 @@ describe('useIsFetching', () => { }) test('should properly update filters', async () => { - const filter = reactive({ stale: false }) + const filter = reactive({ stale: false, queryKey: ['isFetchingFilter'] }) useQuery({ - queryKey: ['isFetching'], - queryFn: () => - new Promise((resolve) => { - setTimeout(() => { - return resolve('Some data') - }, 100) - }), + queryKey: ['isFetchingFilter'], + queryFn: () => sleep(10).then(() => 'Some data'), }) const isFetching = useIsFetching(filter) @@ -84,4 +79,23 @@ describe('useIsFetching', () => { expect(isFetching.value).toStrictEqual(1) }) + + test('should work with options getter and be reactive', async () => { + const staleRef = ref(false) + useQuery({ + queryKey: ['isFetchingGetter'], + queryFn: () => sleep(10).then(() => 'Some data'), + }) + const isFetching = useIsFetching(() => ({ + stale: staleRef.value, + queryKey: ['isFetchingGetter'], + })) + + expect(isFetching.value).toStrictEqual(0) + + staleRef.value = true + await vi.advanceTimersByTimeAsync(0) + + expect(isFetching.value).toStrictEqual(1) + }) }) diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts index be6564608b..73d0fc47e8 100644 --- a/packages/vue-query/src/__tests__/useIsMutating.test.ts +++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' -import { onScopeDispose, reactive } from 'vue-demi' +import { onScopeDispose, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useIsMutating, useMutationState } from '../useMutationState' @@ -88,9 +88,38 @@ describe('useIsMutating', () => { expect(isMutating.value).toStrictEqual(1) }) + + test('should work with options getter and be reactive', async () => { + const keyRef = ref('isMutatingGetter2') + const { mutate } = useMutation({ + mutationKey: ['isMutatingGetter'], + mutationFn: (params: string) => sleep(10).then(() => params), + }) + mutate('foo') + + const isMutating = useIsMutating(() => ({ + mutationKey: [keyRef.value], + })) + + expect(isMutating.value).toStrictEqual(0) + + keyRef.value = 'isMutatingGetter' + + await vi.advanceTimersByTimeAsync(0) + + expect(isMutating.value).toStrictEqual(1) + }) }) describe('useMutationState', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + it('should return variables after calling mutate 1', () => { const mutationKey = ['mutation'] const variables = 'foo123' @@ -127,4 +156,29 @@ describe('useMutationState', () => { expect(mutationState.value[0]?.variables).toEqual(variables) }) + + it('should work with options getter and be reactive', async () => { + const keyRef = ref('useMutationStateGetter2') + const variables = 'foo123' + + const { mutate } = useMutation({ + mutationKey: ['useMutationStateGetter'], + mutationFn: (params: string) => sleep(10).then(() => params), + }) + + mutate(variables) + + const mutationState = useMutationState(() => ({ + filters: { mutationKey: [keyRef.value], status: 'pending' }, + select: (mutation) => mutation.state.variables, + })) + + expect(mutationState.value).toEqual([]) + + keyRef.value = 'useMutationStateGetter' + + await vi.advanceTimersByTimeAsync(0) + + expect(mutationState.value).toEqual([variables]) + }) }) diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts index 55ba96fecf..88991b58dc 100644 --- a/packages/vue-query/src/__tests__/useMutation.test.ts +++ b/packages/vue-query/src/__tests__/useMutation.test.ts @@ -82,6 +82,39 @@ describe('useMutation', () => { }) }) + test('should work with options getter and be reactive', async () => { + const result = 'Mock data' + const keyRef = ref('key01') + const fnMock = vi.fn((params: string) => sleep(10).then(() => params)) + const mutation = useMutation(() => ({ + mutationKey: [keyRef.value], + mutationFn: fnMock, + })) + + mutation.mutate(result) + + await vi.advanceTimersByTimeAsync(10) + + expect(fnMock).toHaveBeenCalledTimes(1) + expect(fnMock).toHaveBeenNthCalledWith( + 1, + result, + expect.objectContaining({ mutationKey: ['key01'] }), + ) + + keyRef.value = 'key02' + await vi.advanceTimersByTimeAsync(0) + mutation.mutate(result) + await vi.advanceTimersByTimeAsync(10) + + expect(fnMock).toHaveBeenCalledTimes(2) + expect(fnMock).toHaveBeenNthCalledWith( + 2, + result, + expect.objectContaining({ mutationKey: ['key02'] }), + ) + }) + test('should update reactive options', async () => { const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() diff --git a/packages/vue-query/src/__tests__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts index 31b98e7b3d..53946da142 100644 --- a/packages/vue-query/src/__tests__/useQueries.test.ts +++ b/packages/vue-query/src/__tests__/useQueries.test.ts @@ -255,7 +255,7 @@ describe('useQueries', () => { }) test('should be `enabled` to accept getter function', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const checked = ref(false) useQueries({ @@ -278,7 +278,7 @@ describe('useQueries', () => { }) test('should allow getters for query keys', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') @@ -307,7 +307,7 @@ describe('useQueries', () => { }) test('should allow arbitrarily nested getters for query keys', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') @@ -368,4 +368,67 @@ describe('useQueries', () => { expect(fetchFn).toHaveBeenCalledTimes(6) }) + + test('should work with options getter and be reactive', async () => { + const fetchFn = vi.fn(() => 'foo') + const key1 = ref('key1') + const key2 = ref('key2') + const key3 = ref('key3') + const key4 = ref('key4') + const key5 = ref('key5') + + useQueries({ + queries: () => [ + { + queryKey: [ + 'key', + key1, + key2.value, + { key: key3.value }, + [{ foo: { bar: key4.value } }], + () => ({ + foo: { + bar: { + baz: key5.value, + }, + }, + }), + ], + queryFn: fetchFn, + }, + ], + }) + + expect(fetchFn).toHaveBeenCalledTimes(1) + + key1.value = 'key1-updated' + + await vi.advanceTimersByTimeAsync(0) + + expect(fetchFn).toHaveBeenCalledTimes(2) + + key2.value = 'key2-updated' + + await vi.advanceTimersByTimeAsync(0) + + expect(fetchFn).toHaveBeenCalledTimes(3) + + key3.value = 'key3-updated' + + await vi.advanceTimersByTimeAsync(0) + + expect(fetchFn).toHaveBeenCalledTimes(4) + + key4.value = 'key4-updated' + + await vi.advanceTimersByTimeAsync(0) + + expect(fetchFn).toHaveBeenCalledTimes(5) + + key5.value = 'key5-updated' + + await vi.advanceTimersByTimeAsync(0) + + expect(fetchFn).toHaveBeenCalledTimes(6) + }) }) diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts index f83ba374a3..316185abde 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -62,6 +62,39 @@ describe('useQuery', () => { }) }) + test('should work with options getter and be reactive', async () => { + const keyRef = ref('key011') + const resultRef = ref('result02') + const query = useQuery(() => ({ + queryKey: [keyRef.value], + queryFn: () => sleep(0).then(() => resultRef.value), + })) + + await vi.advanceTimersByTimeAsync(0) + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'result02' }, + isPending: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + + resultRef.value = 'result021' + keyRef.value = 'key012' + await vi.advanceTimersByTimeAsync(0) + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'result021' }, + isPending: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + }) + test('should return pending status initially', () => { const query = useQuery({ queryKey: ['key1'], @@ -274,7 +307,7 @@ describe('useQuery', () => { }) test('should use the current value for the queryKey when refetch is called', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const keyRef = ref('key11') const query = useQuery({ queryKey: ['key10', keyRef], @@ -302,7 +335,7 @@ describe('useQuery', () => { }) test('should be `enabled` to accept getter function', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const checked = ref(false) useQuery({ @@ -321,7 +354,7 @@ describe('useQuery', () => { }) test('should allow getters for query keys', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') @@ -346,7 +379,7 @@ describe('useQuery', () => { }) test('should allow arbitrarily nested getters for query keys', async () => { - const fetchFn = vi.fn() + const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') diff --git a/packages/vue-query/src/useIsFetching.ts b/packages/vue-query/src/useIsFetching.ts index d4256cea0d..7f67bbeb91 100644 --- a/packages/vue-query/src/useIsFetching.ts +++ b/packages/vue-query/src/useIsFetching.ts @@ -1,14 +1,15 @@ import { getCurrentScope, onScopeDispose, ref, watchEffect } from 'vue-demi' import { useQueryClient } from './useQueryClient' +import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { QueryFilters as QF } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' import type { QueryClient } from './queryClient' -export type QueryFilters = MaybeRefDeep +export type QueryFilters = MaybeRefDeep | (() => MaybeRefDeep) export function useIsFetching( - fetchingFilters: MaybeRefDeep = {}, + fetchingFilters: QueryFilters = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { @@ -24,7 +25,11 @@ export function useIsFetching( const isFetching = ref() const listener = () => { - isFetching.value = client.isFetching(fetchingFilters) + const resolvedFilters = + typeof fetchingFilters === 'function' + ? fetchingFilters() + : fetchingFilters + isFetching.value = client.isFetching(cloneDeepUnref(resolvedFilters)) } const unsubscribe = client.getQueryCache().subscribe(listener) diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 2a2af89aa6..2bb852acbf 100644 --- a/packages/vue-query/src/useMutation.ts +++ b/packages/vue-query/src/useMutation.ts @@ -39,9 +39,13 @@ export type UseMutationOptions< TError = DefaultError, TVariables = void, TOnMutateResult = unknown, -> = MaybeRefDeep< - UseMutationOptionsBase -> +> = + | MaybeRefDeep< + UseMutationOptionsBase + > + | (() => MaybeRefDeep< + UseMutationOptionsBase + >) type MutateSyncFunction< TData = unknown, @@ -77,8 +81,11 @@ export function useMutation< TVariables = void, TOnMutateResult = unknown, >( - mutationOptions: MaybeRefDeep< - UseMutationOptionsBase + mutationOptions: UseMutationOptions< + TData, + TError, + TVariables, + TOnMutateResult >, queryClient?: QueryClient, ): UseMutationReturnType { @@ -92,7 +99,11 @@ export function useMutation< const client = queryClient || useQueryClient() const options = computed(() => { - return client.defaultMutationOptions(cloneDeepUnref(mutationOptions)) + const resolvedOptions = + typeof mutationOptions === 'function' + ? mutationOptions() + : mutationOptions + return client.defaultMutationOptions(cloneDeepUnref(resolvedOptions)) }) const observer = new MutationObserver(client, options.value) const state = options.value.shallow diff --git a/packages/vue-query/src/useMutationState.ts b/packages/vue-query/src/useMutationState.ts index 110f076d47..3395b5fb74 100644 --- a/packages/vue-query/src/useMutationState.ts +++ b/packages/vue-query/src/useMutationState.ts @@ -21,7 +21,7 @@ import type { MutationCache } from './mutationCache' export type MutationFilters = MaybeRefDeep export function useIsMutating( - filters: MutationFilters = {}, + filters: MutationFilters | (() => MutationFilters) = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { @@ -37,7 +37,7 @@ export function useIsMutating( const mutationState = useMutationState( { filters: computed(() => ({ - ...cloneDeepUnref(filters), + ...cloneDeepUnref(typeof filters === 'function' ? filters() : filters), status: 'pending' as const, })), }, @@ -66,18 +66,26 @@ function getResult( } export function useMutationState( - options: MutationStateOptions = {}, + options: + | MutationStateOptions + | (() => MutationStateOptions) = {}, queryClient?: QueryClient, ): Readonly>> { - const filters = computed(() => cloneDeepUnref(options.filters)) + const resolvedOptions = computed(() => { + const newOptions = typeof options === 'function' ? options() : options + return { + filters: cloneDeepUnref(newOptions.filters), + select: newOptions.select, + } + }) const mutationCache = (queryClient || useQueryClient()).getMutationCache() - const state = shallowRef(getResult(mutationCache, options)) + const state = shallowRef(getResult(mutationCache, resolvedOptions.value)) const unsubscribe = mutationCache.subscribe(() => { - state.value = getResult(mutationCache, options) + state.value = getResult(mutationCache, resolvedOptions.value) }) - watch(filters, () => { - state.value = getResult(mutationCache, options) + watch(resolvedOptions, () => { + state.value = getResult(mutationCache, resolvedOptions.value) }) onScopeDispose(() => { diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index 4d06be76d7..f290976a14 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -240,6 +240,7 @@ export function useQueries< ...options }: ShallowOption & { queries: + | (() => MaybeRefDeep>) | MaybeRefDeep> | MaybeRefDeep< readonly [ @@ -261,8 +262,12 @@ export function useQueries< const client = queryClient || useQueryClient() const defaultedQueries = computed(() => { + const resolvedQueries = + typeof queries === 'function' + ? (queries as () => MaybeRefDeep>)() + : queries // Only unref the top level array. - const queriesRaw = unref(queries) as ReadonlyArray + const queriesRaw = unref(resolvedQueries) as ReadonlyArray // Unref the rest for each element in the top level array. return queriesRaw.map((queryOptions) => {