Skip to content

Commit 41009c9

Browse files
feat(vue-query): add support for getters in query keys (#7608)
* feat(vue-query): add support for getters in query keys * add support for nested queries * fix type test * don't export helper function * fix useQueries types * fix lint issue
1 parent c1945ff commit 41009c9

4 files changed

Lines changed: 247 additions & 23 deletions

File tree

packages/vue-query/src/__tests__/useQueries.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,96 @@ describe('useQueries', () => {
275275

276276
expect(fetchFn).toHaveBeenCalled()
277277
})
278+
279+
test('should allow getters for query keys', async () => {
280+
const fetchFn = vi.fn()
281+
const key1 = ref('key1')
282+
const key2 = ref('key2')
283+
284+
useQueries({
285+
queries: [
286+
{
287+
queryKey: ['key', () => key1.value, () => key2.value],
288+
queryFn: fetchFn,
289+
},
290+
],
291+
})
292+
293+
expect(fetchFn).toHaveBeenCalledTimes(1)
294+
295+
key1.value = 'key3'
296+
297+
await flushPromises()
298+
299+
expect(fetchFn).toHaveBeenCalledTimes(2)
300+
301+
key2.value = 'key4'
302+
303+
await flushPromises()
304+
305+
expect(fetchFn).toHaveBeenCalledTimes(3)
306+
})
307+
308+
test('should allow arbitrarily nested getters for query keys', async () => {
309+
const fetchFn = vi.fn()
310+
const key1 = ref('key1')
311+
const key2 = ref('key2')
312+
const key3 = ref('key3')
313+
const key4 = ref('key4')
314+
const key5 = ref('key5')
315+
316+
useQueries({
317+
queries: [
318+
{
319+
queryKey: [
320+
'key',
321+
key1,
322+
() => key2.value,
323+
{ key: () => key3.value },
324+
[{ foo: { bar: () => key4.value } }],
325+
() => ({
326+
foo: {
327+
bar: {
328+
baz: () => key5.value,
329+
},
330+
},
331+
}),
332+
],
333+
queryFn: fetchFn,
334+
},
335+
],
336+
})
337+
338+
expect(fetchFn).toHaveBeenCalledTimes(1)
339+
340+
key1.value = 'key1-updated'
341+
342+
await flushPromises()
343+
344+
expect(fetchFn).toHaveBeenCalledTimes(2)
345+
346+
key2.value = 'key2-updated'
347+
348+
await flushPromises()
349+
350+
expect(fetchFn).toHaveBeenCalledTimes(3)
351+
352+
key3.value = 'key3-updated'
353+
354+
await flushPromises()
355+
356+
expect(fetchFn).toHaveBeenCalledTimes(4)
357+
358+
key4.value = 'key4-updated'
359+
360+
await flushPromises()
361+
362+
expect(fetchFn).toHaveBeenCalledTimes(5)
363+
364+
key5.value = 'key5-updated'
365+
366+
await flushPromises()
367+
368+
expect(fetchFn).toHaveBeenCalledTimes(6)
369+
})
278370
})

packages/vue-query/src/__tests__/useQuery.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,90 @@ describe('useQuery', () => {
287287
expect(fetchFn).toHaveBeenCalled()
288288
})
289289

290+
test('should allow getters for query keys', async () => {
291+
const fetchFn = vi.fn()
292+
const key1 = ref('key1')
293+
const key2 = ref('key2')
294+
295+
useQuery({
296+
queryKey: ['key', () => key1.value, () => key2.value],
297+
queryFn: fetchFn,
298+
})
299+
300+
expect(fetchFn).toHaveBeenCalledTimes(1)
301+
302+
key1.value = 'key3'
303+
304+
await flushPromises()
305+
306+
expect(fetchFn).toHaveBeenCalledTimes(2)
307+
308+
key2.value = 'key4'
309+
310+
await flushPromises()
311+
312+
expect(fetchFn).toHaveBeenCalledTimes(3)
313+
})
314+
315+
test('should allow arbitrarily nested getters for query keys', async () => {
316+
const fetchFn = vi.fn()
317+
const key1 = ref('key1')
318+
const key2 = ref('key2')
319+
const key3 = ref('key3')
320+
const key4 = ref('key4')
321+
const key5 = ref('key5')
322+
323+
useQuery({
324+
queryKey: [
325+
'key',
326+
key1,
327+
() => key2.value,
328+
{ key: () => key3.value },
329+
[{ foo: { bar: () => key4.value } }],
330+
() => ({
331+
foo: {
332+
bar: {
333+
baz: () => key5.value,
334+
},
335+
},
336+
}),
337+
],
338+
queryFn: fetchFn,
339+
})
340+
341+
expect(fetchFn).toHaveBeenCalledTimes(1)
342+
343+
key1.value = 'key1-updated'
344+
345+
await flushPromises()
346+
347+
expect(fetchFn).toHaveBeenCalledTimes(2)
348+
349+
key2.value = 'key2-updated'
350+
351+
await flushPromises()
352+
353+
expect(fetchFn).toHaveBeenCalledTimes(3)
354+
355+
key3.value = 'key3-updated'
356+
357+
await flushPromises()
358+
359+
expect(fetchFn).toHaveBeenCalledTimes(4)
360+
361+
key4.value = 'key4-updated'
362+
363+
await flushPromises()
364+
365+
expect(fetchFn).toHaveBeenCalledTimes(5)
366+
367+
key5.value = 'key5-updated'
368+
369+
await flushPromises()
370+
371+
expect(fetchFn).toHaveBeenCalledTimes(6)
372+
})
373+
290374
describe('throwOnError', () => {
291375
test('should evaluate throwOnError when query is expected to throw', async () => {
292376
const boundaryFn = vi.fn()

packages/vue-query/src/useQueries.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
onScopeDispose,
66
readonly,
77
shallowRef,
8+
unref,
89
watch,
910
} from 'vue-demi'
1011

@@ -268,22 +269,26 @@ export function useQueries<
268269

269270
const client = queryClient || useQueryClient()
270271

271-
const defaultedQueries = computed(() =>
272-
cloneDeepUnref(queries as MaybeRefDeep<UseQueriesOptionsArg<any>>).map(
273-
(queryOptions) => {
274-
if (typeof queryOptions.enabled === 'function') {
275-
queryOptions.enabled = queryOptions.enabled()
276-
}
272+
const defaultedQueries = computed(() => {
273+
// Only unref the top level array.
274+
const queriesRaw = unref(queries) as ReadonlyArray<any>
277275

278-
const defaulted = client.defaultQueryOptions(queryOptions)
279-
defaulted._optimisticResults = client.isRestoring.value
280-
? 'isRestoring'
281-
: 'optimistic'
276+
// Unref the rest for each element in the top level array.
277+
return queriesRaw.map((queryOptions) => {
278+
const clonedOptions = cloneDeepUnref(queryOptions)
282279

283-
return defaulted
284-
},
285-
),
286-
)
280+
if (typeof clonedOptions.enabled === 'function') {
281+
clonedOptions.enabled = queryOptions.enabled()
282+
}
283+
284+
const defaulted = client.defaultQueryOptions(clonedOptions)
285+
defaulted._optimisticResults = client.isRestoring.value
286+
? 'isRestoring'
287+
: 'optimistic'
288+
289+
return defaulted
290+
})
291+
})
287292

288293
const observer = new QueriesObserver<TCombinedResult>(
289294
client,

packages/vue-query/src/utils.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ export function updateState(
1717
})
1818
}
1919

20-
export function cloneDeep<T>(
20+
// Helper function for cloning deep objects where
21+
// the level and key is provided to the callback function.
22+
function _cloneDeep<T>(
2123
value: MaybeRefDeep<T>,
22-
customize?: (val: MaybeRefDeep<T>) => T | undefined,
24+
customize?: (
25+
val: MaybeRefDeep<T>,
26+
key: string,
27+
level: number,
28+
) => T | undefined,
29+
currentKey: string = '',
30+
currentLevel: number = 0,
2331
): T {
2432
if (customize) {
25-
const result = customize(value)
26-
// If it's a ref of undefined, return undefined
33+
const result = customize(value, currentKey, currentLevel)
2734
if (result === undefined && isRef(value)) {
2835
return result as T
2936
}
@@ -33,24 +40,56 @@ export function cloneDeep<T>(
3340
}
3441

3542
if (Array.isArray(value)) {
36-
return value.map((val) => cloneDeep(val, customize)) as unknown as T
43+
return value.map((val, index) =>
44+
_cloneDeep(val, customize, String(index), currentLevel + 1),
45+
) as unknown as T
3746
}
3847

3948
if (typeof value === 'object' && isPlainObject(value)) {
4049
const entries = Object.entries(value).map(([key, val]) => [
4150
key,
42-
cloneDeep(val, customize),
51+
_cloneDeep(val, customize, key, currentLevel + 1),
4352
])
4453
return Object.fromEntries(entries)
4554
}
4655

4756
return value as T
4857
}
4958

50-
export function cloneDeepUnref<T>(obj: MaybeRefDeep<T>): T {
51-
return cloneDeep(obj, (val) => {
59+
export function cloneDeep<T>(
60+
value: MaybeRefDeep<T>,
61+
customize?: (
62+
val: MaybeRefDeep<T>,
63+
key: string,
64+
level: number,
65+
) => T | undefined,
66+
): T {
67+
return _cloneDeep(value, customize)
68+
}
69+
70+
export function cloneDeepUnref<T>(
71+
obj: MaybeRefDeep<T>,
72+
unrefGetters = false,
73+
): T {
74+
return cloneDeep(obj, (val, key, level) => {
75+
// Check if we're at the top level and the key is 'queryKey'
76+
//
77+
// If so, take the recursive descent where we resolve
78+
// getters to values as well as refs.
79+
if (level === 1 && key === 'queryKey') {
80+
return cloneDeepUnref(val, true)
81+
}
82+
83+
// Resolve getters to values if specified.
84+
if (unrefGetters && isFunction(val)) {
85+
// Cast due to older TS versions not allowing calling
86+
// on certain intersection types.
87+
return cloneDeepUnref((val as Function)(), unrefGetters)
88+
}
89+
90+
// Unref refs and continue to recurse into the value.
5291
if (isRef(val)) {
53-
return cloneDeepUnref(unref(val))
92+
return cloneDeepUnref(unref(val), unrefGetters)
5493
}
5594

5695
return undefined
@@ -66,6 +105,10 @@ function isPlainObject(value: unknown): value is Object {
66105
return prototype === null || prototype === Object.prototype
67106
}
68107

108+
function isFunction(value: unknown): value is Function {
109+
return typeof value === 'function'
110+
}
111+
69112
export function shouldThrowError<T extends (...args: Array<any>) => boolean>(
70113
throwOnError: boolean | T | undefined,
71114
params: Parameters<T>,

0 commit comments

Comments
 (0)