diff --git a/docs/react/reference/useQueries.md b/docs/react/reference/useQueries.md index df71e025e0..37e8004e13 100644 --- a/docs/react/reference/useQueries.md +++ b/docs/react/reference/useQueries.md @@ -6,23 +6,50 @@ title: useQueries The `useQueries` hook can be used to fetch a variable number of queries: ```tsx +const ids = [1,2,3] const results = useQueries({ - queries: [ - { queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity}, - { queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity} - ] + queries: ids.map(id => [ + { queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: Infinity }, + ]), }) ``` **Options** -The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](../reference/useQuery) (excluding the `context` option). +The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](../reference/useQuery) (excluding the `queryClient` option - because the `QueryClient` can be passed in on the top level). - `queryClient?: QueryClient`, - - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. + - Use this to provide a custom QueryClient. Otherwise, the one from the nearest context will be used. +- `combine?`: (result: UseQueriesResults) => TCombinedResult + - Use this to combine the results of the queries into a single value. -> Having the same query key more than once in the array of query objects may cause some data to be shared between queries, e.g. when using `placeholderData` and `select`. To avoid this, consider de-duplicating the queries and map the results back to the desired structure. +> Having the same query key more than once in the array of query objects may cause some data to be shared between queries. To avoid this, consider de-duplicating the queries and map the results back to the desired structure. + +**placeholderData** + +The `placeholderData` option exists for `useQueries` as well, but it doesn't get information passed from previously rendered Queries like `useQuery` does, because the input to `useQueries` can be a different number of Queries on each render. **Returns** The `useQueries` hook returns an array with all the query results. The order returned is the same as the input order. + +## Combine + +If you want to combine `data` (or other Query information) from the results into a single value, you can use the `combine` option. The result will be structurally shared to be as referentially stable as possible. + +```tsx +const ids = [1,2,3] +const combinedQueries = useQueries({ + queries: ids.map(id => [ + { queryKey: ['post', id], queryFn: () => fetchPost(id) }, + ]), + combine: (results) => { + return ({ + data: results.map(result => result.data), + pending: results.some(result => result.isPending), + }) + } +}) +``` + +In the above example, `combinedQueries` will be an object with a `data` and a `pending` property. Note that all other properties of the Query results will be lost. diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index 23a9b44681..9c7bf48073 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -36,3 +36,4 @@ export type { DehydratedState, HydrateOptions, } from './hydration' +export type { QueriesObserverOptions } from './queriesObserver' diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index acbd6063cc..212b176dba 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -8,6 +8,7 @@ import type { QueryClient } from './queryClient' import type { NotifyOptions } from './queryObserver' import { QueryObserver } from './queryObserver' import { Subscribable } from './subscribable' +import { replaceEqualDeep } from './utils' function difference(array1: T[], array2: T[]): T[] { return array1.filter((x) => array2.indexOf(x) === -1) @@ -21,23 +22,40 @@ function replaceAt(array: T[], index: number, value: T): T[] { type QueriesObserverListener = (result: QueryObserverResult[]) => void -export class QueriesObserver extends Subscribable { +export interface QueriesObserverOptions< + TCombinedResult = QueryObserverResult[], +> { + combine?: (result: QueryObserverResult[]) => TCombinedResult +} + +export class QueriesObserver< + TCombinedResult = QueryObserverResult[], +> extends Subscribable { #client: QueryClient - #result: QueryObserverResult[] + #result!: QueryObserverResult[] #queries: QueryObserverOptions[] #observers: QueryObserver[] + #options?: QueriesObserverOptions + #combinedResult!: TCombinedResult - constructor(client: QueryClient, queries?: QueryObserverOptions[]) { + constructor( + client: QueryClient, + queries: QueryObserverOptions[], + options?: QueriesObserverOptions, + ) { super() this.#client = client this.#queries = [] - this.#result = [] this.#observers = [] - if (queries) { - this.setQueries(queries) - } + this.#setResult([]) + this.setQueries(queries, options) + } + + #setResult(value: QueryObserverResult[]) { + this.#result = value + this.#combinedResult = this.#combineResult(value) } protected onSubscribe(): void { @@ -65,9 +83,11 @@ export class QueriesObserver extends Subscribable { setQueries( queries: QueryObserverOptions[], + options?: QueriesObserverOptions, notifyOptions?: NotifyOptions, ): void { this.#queries = queries + this.#options = options notifyManager.batch(() => { const prevObservers = this.#observers @@ -92,7 +112,7 @@ export class QueriesObserver extends Subscribable { } this.#observers = newObservers - this.#result = newResult + this.#setResult(newResult) if (!this.hasListeners()) { return @@ -112,8 +132,8 @@ export class QueriesObserver extends Subscribable { }) } - getCurrentResult(): QueryObserverResult[] { - return this.#result + getCurrentResult(): TCombinedResult { + return this.#combinedResult } getQueries() { @@ -124,10 +144,40 @@ export class QueriesObserver extends Subscribable { return this.#observers } - getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] { - return this.#findMatchingObservers(queries).map((match) => + getOptimisticResult( + queries: QueryObserverOptions[], + ): [ + rawResult: QueryObserverResult[], + combineResult: (r?: QueryObserverResult[]) => TCombinedResult, + trackResult: () => QueryObserverResult[], + ] { + const matches = this.#findMatchingObservers(queries) + const result = matches.map((match) => match.observer.getOptimisticResult(match.defaultedQueryOptions), ) + + return [ + result, + (r?: QueryObserverResult[]) => { + return this.#combineResult(r ?? result) + }, + () => { + return matches.map((match, index) => { + const observerResult = result[index]! + return !match.defaultedQueryOptions.notifyOnChangeProps + ? match.observer.trackResult(observerResult) + : observerResult + }) + }, + ] + } + + #combineResult(input: QueryObserverResult[]): TCombinedResult { + const combine = this.#options?.combine + if (combine) { + return replaceEqualDeep(this.#combinedResult, combine(input)) + } + return input as any } #findMatchingObservers( @@ -192,7 +242,7 @@ export class QueriesObserver extends Subscribable { #onUpdate(observer: QueryObserver, result: QueryObserverResult): void { const index = this.#observers.indexOf(observer) if (index !== -1) { - this.#result = replaceAt(this.#result, index, result) + this.#setResult(replaceAt(this.#result, index, result)) this.#notify() } } diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index f86b9d2301..8ddfa512be 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor } from '@testing-library/react' +import { fireEvent, render, waitFor } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' @@ -71,6 +71,52 @@ describe('useQueries', () => { expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) + it('should track results', async () => { + const key1 = queryKey() + const results: UseQueryResult[][] = [] + let count = 0 + + function Page() { + const result = useQueries({ + queries: [ + { + queryKey: key1, + queryFn: async () => { + await sleep(10) + count++ + return count + }, + }, + ], + }) + results.push(result) + + return ( +
+
data: {String(result[0].data ?? 'null')}
+ +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 1')) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject([{ data: undefined }]) + expect(results[1]).toMatchObject([{ data: 1 }]) + + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => rendered.getByText('data: 2')) + + // only one render for data update, no render for isFetching transition + expect(results.length).toBe(3) + + expect(results[2]).toMatchObject([{ data: 2 }]) + }) + it('handles type parameter - tuple of tuples', async () => { const key1 = queryKey() const key2 = queryKey() @@ -802,4 +848,152 @@ describe('useQueries', () => { await waitFor(() => rendered.getByText('data: custom client')) }) + + it('should combine queries', async () => { + const key1 = queryKey() + const key2 = queryKey() + + function Page() { + const queries = useQueries( + { + queries: [ + { + queryKey: key1, + queryFn: () => Promise.resolve('first result'), + }, + { + queryKey: key2, + queryFn: () => Promise.resolve('second result'), + }, + ], + combine: (results) => { + return { + combined: true, + res: results.map((res) => res.data).join(','), + } + }, + }, + queryClient, + ) + + return ( +
+
+ data: {String(queries.combined)} {queries.res} +
+
+ ) + } + + const rendered = render() + + await waitFor(() => + rendered.getByText('data: true first result,second result'), + ) + }) + + it('should track property access through combine function', async () => { + const key1 = queryKey() + const key2 = queryKey() + let count = 0 + const results: Array = [] + + function Page() { + const queries = useQueries( + { + queries: [ + { + queryKey: key1, + queryFn: async () => { + await sleep(10) + return Promise.resolve('first result ' + count) + }, + }, + { + queryKey: key2, + queryFn: async () => { + await sleep(20) + return Promise.resolve('second result ' + count) + }, + }, + ], + combine: (queryResults) => { + return { + combined: true, + refetch: () => queryResults.forEach((res) => res.refetch()), + res: queryResults + .flatMap((res) => (res.data ? [res.data] : [])) + .join(','), + } + }, + }, + queryClient, + ) + + results.push(queries) + + return ( +
+
+ data: {String(queries.combined)} {queries.res} +
+ +
+ ) + } + + const rendered = render() + + await waitFor(() => + rendered.getByText('data: true first result 0,second result 0'), + ) + + expect(results.length).toBe(3) + + expect(results[0]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: '', + }) + + expect(results[1]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 0', + }) + + expect(results[2]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 0,second result 0', + }) + + count++ + + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => + rendered.getByText('data: true first result 1,second result 1'), + ) + + expect(results.length).toBe(5) + + expect(results[3]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 1,second result 0', + }) + + expect(results[4]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 1,second result 1', + }) + + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await sleep(50) + // no further re-render because data didn't change + expect(results.length).toBe(5) + }) }) diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 517479a782..50843fc5a4 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -7,6 +7,7 @@ import type { QueriesPlaceholderDataFunction, QueryClient, DefaultError, + QueriesObserverOptions, } from '@tanstack/query-core' import { notifyManager, QueriesObserver } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' @@ -156,22 +157,27 @@ export type QueriesResults< : // Fallback UseQueryResult[] -export function useQueries( +export function useQueries< + T extends any[], + TCombinedResult = QueriesResults, +>( { queries, + ...options }: { queries: readonly [...QueriesOptions] + combine?: (result: QueriesResults) => TCombinedResult }, queryClient?: QueryClient, -): QueriesResults { +): TCombinedResult { const client = useQueryClient(queryClient) const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() const defaultedQueries = React.useMemo( () => - queries.map((options) => { - const defaultedOptions = client.defaultQueryOptions(options) + queries.map((opts) => { + const defaultedOptions = client.defaultQueryOptions(opts) // Make sure the results are already in fetching state before subscribing or updating options defaultedOptions._optimisticResults = isRestoring @@ -191,10 +197,16 @@ export function useQueries( useClearResetErrorBoundary(errorResetBoundary) const [observer] = React.useState( - () => new QueriesObserver(client, defaultedQueries), + () => + new QueriesObserver( + client, + defaultedQueries, + options as QueriesObserverOptions, + ), ) - const optimisticResult = observer.getOptimisticResult(defaultedQueries) + const [optimisticResult, getCombinedResult, trackResult] = + observer.getOptimisticResult(defaultedQueries) React.useSyncExternalStore( React.useCallback( @@ -211,8 +223,14 @@ export function useQueries( React.useEffect(() => { // Do not notify on updates because of changes in the options because // these changes should already be reflected in the optimistic result. - observer.setQueries(defaultedQueries, { listeners: false }) - }, [defaultedQueries, observer]) + observer.setQueries( + defaultedQueries, + options as QueriesObserverOptions, + { + listeners: false, + }, + ) + }, [defaultedQueries, options, observer]) const shouldAtLeastOneSuspend = optimisticResult.some((result, index) => shouldSuspend(defaultedQueries[index], result, isRestoring), @@ -220,14 +238,14 @@ export function useQueries( const suspensePromises = shouldAtLeastOneSuspend ? optimisticResult.flatMap((result, index) => { - const options = defaultedQueries[index] + const opts = defaultedQueries[index] const queryObserver = observer.getObservers()[index] - if (options && queryObserver) { - if (shouldSuspend(options, result, isRestoring)) { - return fetchOptimistic(options, queryObserver, errorResetBoundary) + if (opts && queryObserver) { + if (shouldSuspend(opts, result, isRestoring)) { + return fetchOptimistic(opts, queryObserver, errorResetBoundary) } else if (willFetch(result, isRestoring)) { - void fetchOptimistic(options, queryObserver, errorResetBoundary) + void fetchOptimistic(opts, queryObserver, errorResetBoundary) } } return [] @@ -252,5 +270,5 @@ export function useQueries( throw firstSingleResultWhichShouldThrow.error } - return optimisticResult as QueriesResults + return getCombinedResult(trackResult()) } diff --git a/packages/solid-query/src/__tests__/createQueries.test.tsx b/packages/solid-query/src/__tests__/createQueries.test.tsx index c301b48034..4cec6f2913 100644 --- a/packages/solid-query/src/__tests__/createQueries.test.tsx +++ b/packages/solid-query/src/__tests__/createQueries.test.tsx @@ -672,7 +672,7 @@ describe('useQueries', () => { const QueriesObserverSpy = vi .spyOn(QueriesObserverModule, 'QueriesObserver') .mockImplementation((fn) => { - return new QueriesObserverMock(fn) + return new QueriesObserverMock(fn, []) }) function Queries() { diff --git a/packages/solid-query/src/createQueries.ts b/packages/solid-query/src/createQueries.ts index ac94422099..64889f80c1 100644 --- a/packages/solid-query/src/createQueries.ts +++ b/packages/solid-query/src/createQueries.ts @@ -3,11 +3,12 @@ import type { QueryFunction, QueryKey, DefaultError, + QueriesObserverOptions, } from '@tanstack/query-core' import { notifyManager, QueriesObserver } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' import type { Accessor } from 'solid-js' -import { createComputed, onCleanup, onMount } from 'solid-js' +import { createComputed, onCleanup } from 'solid-js' import { createStore, unwrap } from 'solid-js/store' import { useQueryClient } from './QueryClientProvider' import type { CreateQueryResult, SolidQueryOptions } from './types' @@ -148,12 +149,16 @@ export type QueriesResults< : // Fallback CreateQueryResult[] -export function createQueries( +export function createQueries< + T extends any[], + TCombinedResult = QueriesResults, +>( queriesOptions: Accessor<{ queries: readonly [...QueriesOptions] + combine?: (result: QueriesResults) => TCombinedResult }>, queryClient?: Accessor, -): QueriesResults { +): TCombinedResult { const client = useQueryClient(queryClient?.()) const defaultedQueries = queriesOptions().queries.map((options) => { @@ -162,32 +167,45 @@ export function createQueries( return defaultedOptions }) - const observer = new QueriesObserver(client, defaultedQueries) + const observer = new QueriesObserver( + client, + defaultedQueries, + queriesOptions().combine + ? ({ + combine: queriesOptions().combine, + } as QueriesObserverOptions) + : undefined, + ) - const [state, setState] = createStore( - observer.getOptimisticResult(defaultedQueries), + // @ts-expect-error - Types issue with solid-js createStore + const [state, setState] = createStore( + observer.getOptimisticResult(defaultedQueries)[1](), ) const unsubscribe = observer.subscribe((result) => { notifyManager.batchCalls(() => { - setState(unwrap(result)) + setState(unwrap(result) as unknown as TCombinedResult) })() }) onCleanup(unsubscribe) - onMount(() => { - observer.setQueries(defaultedQueries, { listeners: false }) - }) - createComputed(() => { const updatedQueries = queriesOptions().queries.map((options) => { const defaultedOptions = client.defaultQueryOptions(options) defaultedOptions._optimisticResults = 'optimistic' return defaultedOptions }) - observer.setQueries(updatedQueries) + observer.setQueries( + updatedQueries, + queriesOptions().combine + ? ({ + combine: queriesOptions().combine, + } as QueriesObserverOptions) + : undefined, + { listeners: false }, + ) }) - return state as QueriesResults + return state } diff --git a/packages/svelte-query/src/createQueries.ts b/packages/svelte-query/src/createQueries.ts index 868670ae01..ac5e0de48c 100644 --- a/packages/svelte-query/src/createQueries.ts +++ b/packages/svelte-query/src/createQueries.ts @@ -5,6 +5,7 @@ import type { QueriesPlaceholderDataFunction, QueryObserverResult, DefaultError, + QueriesObserverOptions, } from '@tanstack/query-core' import { notifyManager, QueriesObserver } from '@tanstack/query-core' @@ -150,44 +151,56 @@ export type QueriesResults< : // Fallback QueryObserverResult[] -export type CreateQueriesResult = Readable> - -export function createQueries( +export function createQueries< + T extends any[], + TCombinedResult = QueriesResults, +>( { queries, + ...options }: { queries: WritableOrVal<[...QueriesOptions]> + combine?: (result: QueriesResults) => TCombinedResult }, queryClient?: QueryClient, -): CreateQueriesResult { +): Readable { const client = useQueryClient(queryClient) // const isRestoring = useIsRestoring() const queriesStore = isWritable(queries) ? queries : writable(queries) const defaultedQueriesStore = derived(queriesStore, ($queries) => { - return $queries.map((options) => { - const defaultedOptions = client.defaultQueryOptions(options) + return $queries.map((opts) => { + const defaultedOptions = client.defaultQueryOptions(opts) // Make sure the results are already in fetching state before subscribing or updating options defaultedOptions._optimisticResults = 'optimistic' return defaultedOptions }) }) - const observer = new QueriesObserver(client, get(defaultedQueriesStore)) + const observer = new QueriesObserver( + client, + get(defaultedQueriesStore), + options as QueriesObserverOptions, + ) defaultedQueriesStore.subscribe(($defaultedQueries) => { // Do not notify on updates because of changes in the options because // these changes should already be reflected in the optimistic result. - observer.setQueries($defaultedQueries, { listeners: false }) + observer.setQueries( + $defaultedQueries, + options as QueriesObserverOptions, + { listeners: false }, + ) }) - const { subscribe } = readable( - observer.getOptimisticResult(get(defaultedQueriesStore)) as any, - (set) => { - return observer.subscribe(notifyManager.batchCalls(set)) - }, + const [, getCombinedResult] = observer.getOptimisticResult( + get(defaultedQueriesStore), ) + const { subscribe } = readable(getCombinedResult() as any, (set) => { + return observer.subscribe(notifyManager.batchCalls(set)) + }) + return { subscribe } } diff --git a/packages/vue-query/src/__tests__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts index 106d84ac39..890b0117de 100644 --- a/packages/vue-query/src/__tests__/useQueries.test.ts +++ b/packages/vue-query/src/__tests__/useQueries.test.ts @@ -217,4 +217,40 @@ describe('useQueries', () => { expect(useQueryClient).toHaveBeenCalledTimes(0) }) + + test('should combine queries', async () => { + const firstResult = 'first result' + const secondResult = 'second result' + + const queryClient = new QueryClient() + const queries = [ + { + queryKey: ['key41'], + queryFn: getSimpleFetcherWithReturnData(firstResult), + }, + { + queryKey: ['key42'], + queryFn: getSimpleFetcherWithReturnData(secondResult), + }, + ] + + const queriesResult = useQueries( + { + queries, + combine: (results) => { + return { + combined: true, + res: results.map((res) => res.data), + } + }, + }, + queryClient, + ) + await flushPromises() + + expect(queriesResult.value).toMatchObject({ + combined: true, + res: [firstResult, secondResult], + }) + }) }) diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index 9d29fa5bdd..f7ed69763d 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -3,6 +3,7 @@ import { QueriesObserver } from '@tanstack/query-core' import type { QueriesPlaceholderDataFunction, QueryKey, + QueriesObserverOptions, } from '@tanstack/query-core' import type { Ref } from 'vue-demi' import { computed, onScopeDispose, readonly, ref, watch } from 'vue-demi' @@ -145,19 +146,24 @@ export type UseQueriesResults< type UseQueriesOptionsArg = readonly [...UseQueriesOptions] -export function useQueries( +export function useQueries< + T extends any[], + TCombinedResult = UseQueriesResults, +>( { queries, + ...options }: { queries: MaybeRefDeep> + combine?: (result: UseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, -): Readonly>> { +): Readonly> { const client = queryClient || useQueryClient() const defaultedQueries = computed(() => - cloneDeepUnref(queries).map((options) => { - const defaulted = client.defaultQueryOptions(options) + cloneDeepUnref(queries).map((queryOptions) => { + const defaulted = client.defaultQueryOptions(queryOptions) defaulted._optimisticResults = client.isRestoring.value ? 'isRestoring' : 'optimistic' @@ -166,8 +172,15 @@ export function useQueries( }), ) - const observer = new QueriesObserver(client, defaultedQueries.value) - const state = ref(observer.getCurrentResult()) + const observer = new QueriesObserver( + client, + defaultedQueries.value, + options as QueriesObserverOptions, + ) + const [, getCombinedResult] = observer.getOptimisticResult( + defaultedQueries.value, + ) + const state = ref(getCombinedResult()) as Ref const unsubscribe = ref(() => { // noop @@ -178,20 +191,29 @@ export function useQueries( (isRestoring) => { if (!isRestoring) { unsubscribe.value() - unsubscribe.value = observer.subscribe((result) => { - state.value.splice(0, result.length, ...result) + unsubscribe.value = observer.subscribe(() => { + const [, getCombinedResultRestoring] = observer.getOptimisticResult( + defaultedQueries.value, + ) + state.value = getCombinedResultRestoring() }) // Subscription would not fire for persisted results - state.value = observer.getOptimisticResult(defaultedQueries.value) + const [, getCombinedResultPersisted] = observer.getOptimisticResult( + defaultedQueries.value, + ) + state.value = getCombinedResultPersisted() } }, { immediate: true }, ) watch( - defaultedQueries, + [defaultedQueries], () => { - observer.setQueries(defaultedQueries.value) + observer.setQueries( + defaultedQueries.value, + options as QueriesObserverOptions, + ) state.value = observer.getCurrentResult() }, { deep: true }, @@ -201,5 +223,5 @@ export function useQueries( unsubscribe.value() }) - return readonly(state) as Readonly>> + return readonly(state) as Readonly> }