Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_WFMzIozQ08ypLxNWRk6Bk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"packages/react-query/package.json":"Patch"},"note":"Add useQueries","date":"2026-01-13T03:27:15.760011900Z"}
6 changes: 6 additions & 0 deletions packages/react-query/src/__tests__/query-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ test('DevupQueryClient useInfiniteQuery method exists', () => {
expect(typeof queryClient.useInfiniteQuery).toBe('function')
})

test('DevupQueryClient useQueries method exists', () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)
expect(typeof queryClient.useQueries).toBe('function')
})

test('getQueryKey returns correct key without options', () => {
const result = getQueryKey('get', '/test', undefined)
expect(result).toEqual(['get', '/test'])
Expand Down
133 changes: 133 additions & 0 deletions packages/react-query/src/__tests__/query-client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,136 @@ test('DevupQueryClient useInfiniteQuery with different HTTP methods', async () =
})
}
})

test('DevupQueryClient useQueries with multiple queries', async () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)

const { result } = renderHook(
() =>
queryClient.useQueries([
['get', '/test1'],
['get', '/test2'],
]),
{ wrapper: createWrapper() },
)

await waitFor(
() => {
expect(result.current.every((r) => r.isSuccess)).toBe(true)
},
{ timeout: 5000 },
)

expect(result.current).toHaveLength(2)
expect(result.current[0].data).toEqual({ id: 1, name: 'test' })
expect(result.current[1].data).toEqual({ id: 1, name: 'test' })
})

test('DevupQueryClient useQueries with options', async () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)

const { result } = renderHook(
() =>
queryClient.useQueries([
['get', '/test' as any, { params: { id: '123' } }],
]),
{ wrapper: createWrapper() },
)

await waitFor(
() => {
expect(result.current[0].isSuccess).toBe(true)
},
{ timeout: 5000 },
)

expect(result.current[0].data).toEqual({ id: 1, name: 'test' })
})

test('DevupQueryClient useQueries with combine', async () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)

const { result } = renderHook(
() =>
queryClient.useQueries(
[
['get', '/test1' as any],
['get', '/test2' as any],
],
{
combine: (results) => ({
data: results.map((r) => r.data),
pending: results.some((r) => r.isPending),
}),
},
),
{ wrapper: createWrapper() },
)

await waitFor(
() => {
expect(result.current.pending).toBe(false)
},
{ timeout: 5000 },
)

expect(result.current.data).toEqual([
{ id: 1, name: 'test' },
{ id: 1, name: 'test' },
])
})

test('DevupQueryClient useQueries with different HTTP methods', async () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)

const { result } = renderHook(
() =>
queryClient.useQueries([
['get', '/test' as any],
['GET', '/test' as any],
['post', '/test' as any],
]),
{ wrapper: createWrapper() },
)

await waitFor(
() => {
expect(result.current.every((r) => r.isSuccess)).toBe(true)
},
{ timeout: 5000 },
)

expect(result.current).toHaveLength(3)
})

test('DevupQueryClient useQueries with queryOptions', async () => {
const api = createApi({ baseUrl: 'https://api.example.com' })
const queryClient = new DevupQueryClient(api)

const { result } = renderHook(
() =>
queryClient.useQueries([
['get', '/test' as any, undefined, { staleTime: 1000 }],
[
'get',
'/test2' as any,
{ params: { id: '123' } },
{ staleTime: 2000 },
],
]),
{ wrapper: createWrapper() },
)

await waitFor(
() => {
expect(result.current.every((r) => r.isSuccess)).toBe(true)
},
{ timeout: 5000 },
)

expect(result.current).toHaveLength(2)
})
80 changes: 80 additions & 0 deletions packages/react-query/src/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import {
useInfiniteQuery,
useMutation,
useQueries,
useQuery,
useSuspenseQuery,
} from '@tanstack/react-query'
Expand Down Expand Up @@ -299,4 +300,83 @@ export class DevupQueryClient<S extends ConditionalKeys<DevupApiServers>> {
options[2],
)
}

useQueries<
M extends
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH',
ST extends {
get: DevupGetApiStructScope<S>
post: DevupPostApiStructScope<S>
put: DevupPutApiStructScope<S>
delete: DevupDeleteApiStructScope<S>
patch: DevupPatchApiStructScope<S>
GET: DevupGetApiStructScope<S>
POST: DevupPostApiStructScope<S>
PUT: DevupPutApiStructScope<S>
DELETE: DevupDeleteApiStructScope<S>
PATCH: DevupPatchApiStructScope<S>
}[M],
T extends ConditionalKeys<ST>,
O extends Additional<T, ST>,
D extends ExtractValue<O, 'response'>,
E extends ExtractValue<O, 'error'>,
TCombinedResult = Array<ReturnType<typeof useQuery<D, E>>>,
>(
queries: Array<
[
method: M,
path: T,
options?: ConditionalApiOption<O>,
queryOptions?: Omit<
Parameters<typeof useQuery<D, E>>[0],
'queryFn' | 'queryKey'
>,
]
>,
options?: {
combine?: (
results: Array<ReturnType<typeof useQuery<D, E>>>,
) => TCombinedResult
queryClient?: Parameters<typeof useQueries>[1]
},
): TCombinedResult {
return useQueries(
{
queries: queries.map(([method, path, apiOptions, queryOptions]) => ({
queryKey: getQueryKey(method, path, apiOptions),
queryFn: ({
queryKey: [methodKey, pathKey, ...restOptions],
signal,
}: {
queryKey: [M, T, ...unknown[]]
signal: AbortSignal
}): Promise<D> =>
// biome-ignore lint/suspicious/noExplicitAny: can't use method as a function
(this.api as any)
[methodKey as string](pathKey, {
signal,
...(restOptions[0] as DevupApiRequestInit),
})
.then(({ data, error }: DevupApiResponse<D, E>) => {
if (error) throw error
return data
}),
...queryOptions,
})) as Parameters<typeof useQueries>[0]['queries'],
combine: options?.combine as Parameters<
typeof useQueries
>[0]['combine'],
},
options?.queryClient,
) as TCombinedResult
}
}