diff --git a/docs/openapi-react-query/use-infinite-query.md b/docs/openapi-react-query/use-infinite-query.md
new file mode 100644
index 000000000..86bcbad1e
--- /dev/null
+++ b/docs/openapi-react-query/use-infinite-query.md
@@ -0,0 +1,112 @@
+---
+title: useInfiniteQuery
+---
+
+# {{ $frontmatter.title }}
+
+The `useInfiniteQuery` method allows you to use the original [useInfiniteQuery](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries)
+
+- The result is the same as the original function.
+- The `queryKey` is `[method, path, params]`.
+- `data` and `error` are fully typed.
+- You can pass infinite query options as fourth parameter.
+
+::: tip
+You can find more information about `useInfiniteQuery` on the [@tanstack/react-query documentation](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries).
+:::
+
+## Example
+
+::: code-group
+
+```tsx [src/app.tsx]
+import { $api } from "./api";
+const PostList = () => {
+ const { data, fetchNextPage, hasNextPage, isFetching } =
+ $api.useInfiniteQuery(
+ "get",
+ "/posts",
+ {
+ params: {
+ query: {
+ limit: 10,
+ },
+ },
+ },
+ {
+ getNextPageParam: (lastPage) => lastPage.nextPage,
+ initialPageParam: 0,
+ }
+ );
+
+ return (
+
+ {data?.pages.map((page, i) => (
+
+ {page.items.map((post) => (
+
{post.title}
+ ))}
+
+ ))}
+ {hasNextPage && (
+
+ )}
+
+ );
+};
+
+export const App = () => {
+ return (
+ `Error: ${error.message}`}>
+
+
+ );
+};
+```
+
+```ts [src/api.ts]
+import createFetchClient from "openapi-fetch";
+import createClient from "openapi-react-query";
+import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
+
+const fetchClient = createFetchClient({
+ baseUrl: "https://myapi.dev/v1/",
+});
+export const $api = createClient(fetchClient);
+```
+
+:::
+
+## Api
+
+```tsx
+const query = $api.useInfiniteQuery(
+ method,
+ path,
+ options,
+ infiniteQueryOptions,
+ queryClient
+);
+```
+
+**Arguments**
+
+- `method` **(required)**
+ - The HTTP method to use for the request.
+ - The method is used as key. See [Query Keys](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) for more information.
+- `path` **(required)**
+ - The pathname to use for the request.
+ - Must be an available path for the given method in your schema.
+ - The pathname is used as key. See [Query Keys](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) for more information.
+- `options`
+ - The fetch options to use for the request.
+ - Only required if the OpenApi schema requires parameters.
+ - The options `params` are used as key. See [Query Keys](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) for more information.
+- `infiniteQueryOptions`
+ - The original `useInfiniteQuery` options.
+ - [See more information](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery)
+- `queryClient`
+ - The original `queryClient` option.
+ - [See more information](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery)
diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts
index cbee066c0..1aba524b9 100644
--- a/packages/openapi-react-query/src/index.ts
+++ b/packages/openapi-react-query/src/index.ts
@@ -5,12 +5,15 @@ import {
type UseQueryResult,
type UseSuspenseQueryOptions,
type UseSuspenseQueryResult,
+ type UseInfiniteQueryOptions,
+ type UseInfiniteQueryResult,
type QueryClient,
type QueryFunctionContext,
type SkipToken,
useMutation,
useQuery,
useSuspenseQuery,
+ useInfiniteQuery,
} from "@tanstack/react-query";
import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch";
import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers";
@@ -84,6 +87,45 @@ export type UseSuspenseQueryMethod, Options?, QueryClient?]
) => UseSuspenseQueryResult;
+export type UseInfiniteQueryMethod>, Media extends MediaType> = <
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit,
+ Response extends Required>,
+>(
+ method: Method,
+ url: Path,
+ ...[init, options, queryClient]: RequiredKeysOf extends never
+ ? [
+ InitWithUnknowns?,
+ Omit<
+ UseInfiniteQueryOptions<
+ Response["data"],
+ Response["error"],
+ Response["data"],
+ number,
+ QueryKey
+ >,
+ "queryKey" | "queryFn"
+ >?,
+ QueryClient?,
+ ]
+ : [
+ InitWithUnknowns,
+ Omit<
+ UseInfiniteQueryOptions<
+ Response["data"],
+ Response["error"],
+ Response["data"],
+ number,
+ QueryKey
+ >,
+ "queryKey" | "queryFn"
+ >?,
+ QueryClient?,
+ ]
+) => UseInfiniteQueryResult;
+
export type UseMutationMethod>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod,
@@ -101,6 +143,7 @@ export interface OpenapiQueryClient;
useQuery: UseQueryMethod;
useSuspenseQuery: UseSuspenseQueryMethod;
+ useInfiniteQuery: UseInfiniteQueryMethod;
useMutation: UseMutationMethod;
}
@@ -128,12 +171,49 @@ export default function createClient,
+ Init extends MaybeOptionalInit,
+ Response extends Required>,
+ >(
+ method: Method,
+ path: Path,
+ init?: InitWithUnknowns,
+ options?: Omit<
+ UseInfiniteQueryOptions<
+ Response["data"],
+ Response["error"],
+ Response["data"],
+ number,
+ QueryKey
+ >,
+ "queryKey" | "queryFn"
+ >,
+ ) => ({
+ queryKey: [method, path, init] as const,
+ queryFn,
+ ...options,
+ });
+
return {
queryOptions,
useQuery: (method, path, ...[init, options, queryClient]) =>
useQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient),
useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient),
+ useInfiniteQuery: (method, path, ...[init, options, queryClient]) => {
+ const baseOptions = infiniteQueryOptions(method, path, init as InitWithUnknowns, options as any); // TODO: find a way to avoid as any
+ return useInfiniteQuery(
+ {
+ ...baseOptions,
+ initialPageParam: 0,
+ getNextPageParam: (lastPage: any, allPages: any[], lastPageParam: number, allPageParams: number[]) =>
+ options?.getNextPageParam?.(lastPage, allPages, lastPageParam, allPageParams) ?? allPages.length,
+ } as any,
+ queryClient,
+ );
+ },
useMutation: (method, path, options, queryClient) =>
useMutation(
{
diff --git a/packages/openapi-react-query/test/fixtures/api.d.ts b/packages/openapi-react-query/test/fixtures/api.d.ts
index cad5160d4..463ce893f 100644
--- a/packages/openapi-react-query/test/fixtures/api.d.ts
+++ b/packages/openapi-react-query/test/fixtures/api.d.ts
@@ -4,6 +4,58 @@
*/
export interface paths {
+ "/paginated-data": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query: {
+ limit: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ items?: number[];
+ nextPage?: number;
+ };
+ };
+ };
+ /** @description Error response */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ code?: number;
+ message?: string;
+ };
+ };
+ };
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/comment": {
parameters: {
query?: never;
diff --git a/packages/openapi-react-query/test/fixtures/api.yaml b/packages/openapi-react-query/test/fixtures/api.yaml
index 8994e1ba8..62b4c5c60 100644
--- a/packages/openapi-react-query/test/fixtures/api.yaml
+++ b/packages/openapi-react-query/test/fixtures/api.yaml
@@ -3,6 +3,39 @@ info:
title: Test Specification
version: "1.0"
paths:
+ /paginated-data:
+ get:
+ parameters:
+ - in: query
+ name: limit
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ type: integer
+ nextPage:
+ type: integer
+ '500':
+ description: Error response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ code:
+ type: integer
+ message:
+ type: string
/comment:
put:
requestBody:
diff --git a/packages/openapi-react-query/test/index.test.tsx b/packages/openapi-react-query/test/index.test.tsx
index 294175df9..a2ad01efc 100644
--- a/packages/openapi-react-query/test/index.test.tsx
+++ b/packages/openapi-react-query/test/index.test.tsx
@@ -3,7 +3,7 @@ import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server.j
import type { paths } from "./fixtures/api.js";
import createClient from "../src/index.js";
import createFetchClient from "openapi-fetch";
-import { fireEvent, render, renderHook, screen, waitFor, act } from "@testing-library/react";
+import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
import {
QueryClient,
QueryClientProvider,
@@ -789,4 +789,65 @@ describe("client", () => {
});
});
});
+ describe("useInfiniteQuery", () => {
+ it("should fetch data correctly with pagination", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/paginated-data",
+ status: 200,
+ body: { items: [1, 2, 3], nextPage: 1 },
+ });
+
+ const { result } = renderHook(
+ () => client.useInfiniteQuery("get", "/paginated-data", { params: { query: { limit: 3 } } }),
+ { wrapper },
+ );
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect((result.current.data as any).pages[0]).toEqual({ items: [1, 2, 3], nextPage: 1 });
+
+ // Set up mock for second page
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/paginated-data",
+ status: 200,
+ body: { items: [4, 5, 6], nextPage: 2 },
+ });
+
+ await result.current.fetchNextPage();
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ expect((result.current.data as any).pages).toHaveLength(2);
+ expect((result.current.data as any).pages[1]).toEqual({ items: [4, 5, 6], nextPage: 2 });
+ });
+
+ it("should handle errors correctly", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/paginated-data",
+ status: 500,
+ body: { code: 500, message: "Internal Server Error" },
+ });
+
+ const { result } = renderHook(
+ () => client.useInfiniteQuery("get", "/paginated-data", { params: { query: { limit: 3 } } }),
+ { wrapper },
+ );
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+
+ expect(result.current.error).toEqual({ code: 500, message: "Internal Server Error" });
+ });
+ });
});