From 2d7495b3642ffdb39c86d143513eb57de5cf0951 Mon Sep 17 00:00:00 2001 From: jungwoo3490 Date: Sun, 15 Dec 2024 05:49:50 +0900 Subject: [PATCH 1/5] feat(react-query): add useInfiniteQuery --- packages/openapi-react-query/src/index.ts | 57 ++++++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index cbee066c0..06b1a28b7 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -1,18 +1,21 @@ import { - type UseMutationOptions, - type UseMutationResult, - type UseQueryOptions, - type UseQueryResult, - type UseSuspenseQueryOptions, - type UseSuspenseQueryResult, type QueryClient, type QueryFunctionContext, type SkipToken, + useInfiniteQuery, + type UseInfiniteQueryOptions, + type UseInfiniteQueryResult, useMutation, + type UseMutationOptions, + type UseMutationResult, useQuery, + type UseQueryOptions, + type UseQueryResult, useSuspenseQuery, + type UseSuspenseQueryOptions, + type UseSuspenseQueryResult } from "@tanstack/react-query"; -import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch"; +import type { ClientMethod, Client as FetchClient, FetchResponse, MaybeOptionalInit } from "openapi-fetch"; import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; type InitWithUnknowns = Init & { [key: string]: unknown }; @@ -84,6 +87,19 @@ 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>, "queryKey" | "queryFn">?, QueryClient?] + : [InitWithUnknowns, Omit>, "queryKey" | "queryFn">?, QueryClient?] +) => UseInfiniteQueryResult; + export type UseMutationMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, @@ -101,6 +117,7 @@ export interface OpenapiQueryClient; useQuery: UseQueryMethod; useSuspenseQuery: UseSuspenseQueryMethod; + useInfiniteQuery: UseInfiniteQueryMethod; useMutation: UseMutationMethod; } @@ -128,12 +145,38 @@ export default function createClient, + Init extends MaybeOptionalInit, + Response extends Required>, +>( + method: Method, + path: Path, + init?: InitWithUnknowns, + options?: Omit>, "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( { From 5b61eeadd81524b939827248045bf338098e3e34 Mon Sep 17 00:00:00 2001 From: jungwoo3490 Date: Sun, 15 Dec 2024 05:50:16 +0900 Subject: [PATCH 2/5] feat(react-query): add unit test for useInfiniteQuery --- .../test/fixtures/api.d.ts | 52 +++++++++++++ .../test/fixtures/api.yaml | 33 ++++++++ .../openapi-react-query/test/index.test.tsx | 75 +++++++++++++++++-- 3 files changed, 153 insertions(+), 7 deletions(-) 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..8239a9998 100644 --- a/packages/openapi-react-query/test/index.test.tsx +++ b/packages/openapi-react-query/test/index.test.tsx @@ -1,19 +1,19 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server.js"; -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 { QueryClient, QueryClientProvider, + skipToken, useQueries, useQuery, useSuspenseQuery, - skipToken, } from "@tanstack/react-query"; +import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react"; +import createFetchClient from "openapi-fetch"; import { Suspense, type ReactNode } from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import createClient from "../src/index.js"; +import type { paths } from "./fixtures/api.js"; +import { baseUrl, server, useMockRequestHandler } from "./fixtures/mock-server.js"; type minimalGetPaths = { // Without parameters. @@ -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" }); + }); + }); }); From ba441ca5fa1d34c55cdcf01509140e025faf720f Mon Sep 17 00:00:00 2001 From: jungwoo3490 Date: Sun, 15 Dec 2024 06:03:34 +0900 Subject: [PATCH 3/5] fix(react-query): revert unnecessary import order change --- packages/openapi-react-query/src/index.ts | 20 +++++++++---------- .../openapi-react-query/test/index.test.tsx | 14 ++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index 06b1a28b7..a9d2503b0 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -1,21 +1,21 @@ import { + type UseMutationOptions, + type UseMutationResult, + type UseQueryOptions, + type UseQueryResult, + type UseSuspenseQueryOptions, + type UseSuspenseQueryResult, + type UseInfiniteQueryOptions, + type UseInfiniteQueryResult, type QueryClient, type QueryFunctionContext, type SkipToken, - useInfiniteQuery, - type UseInfiniteQueryOptions, - type UseInfiniteQueryResult, useMutation, - type UseMutationOptions, - type UseMutationResult, useQuery, - type UseQueryOptions, - type UseQueryResult, useSuspenseQuery, - type UseSuspenseQueryOptions, - type UseSuspenseQueryResult + useInfiniteQuery, } from "@tanstack/react-query"; -import type { ClientMethod, Client as FetchClient, FetchResponse, MaybeOptionalInit } from "openapi-fetch"; +import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch"; import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; type InitWithUnknowns = Init & { [key: string]: unknown }; diff --git a/packages/openapi-react-query/test/index.test.tsx b/packages/openapi-react-query/test/index.test.tsx index 8239a9998..a2ad01efc 100644 --- a/packages/openapi-react-query/test/index.test.tsx +++ b/packages/openapi-react-query/test/index.test.tsx @@ -1,19 +1,19 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server.js"; +import type { paths } from "./fixtures/api.js"; +import createClient from "../src/index.js"; +import createFetchClient from "openapi-fetch"; +import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider, - skipToken, useQueries, useQuery, useSuspenseQuery, + skipToken, } from "@tanstack/react-query"; -import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react"; -import createFetchClient from "openapi-fetch"; import { Suspense, type ReactNode } from "react"; import { ErrorBoundary } from "react-error-boundary"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import createClient from "../src/index.js"; -import type { paths } from "./fixtures/api.js"; -import { baseUrl, server, useMockRequestHandler } from "./fixtures/mock-server.js"; type minimalGetPaths = { // Without parameters. From dabc80012fd299afc56823f43bf98892300ff317 Mon Sep 17 00:00:00 2001 From: jungwoo3490 Date: Sun, 15 Dec 2024 16:04:12 +0900 Subject: [PATCH 4/5] docs(react-query): add useInfiniteQuery docs --- .../openapi-react-query/use-infinite-query.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/openapi-react-query/use-infinite-query.md 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) From 688bc2f7003299272c162cb2f15947cd5429aa88 Mon Sep 17 00:00:00 2001 From: jungwoo3490 Date: Sun, 15 Dec 2024 16:13:16 +0900 Subject: [PATCH 5/5] fix(react-query): fix lint ci error --- packages/openapi-react-query/src/index.ts | 83 ++++++++++++++++------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index a9d2503b0..1aba524b9 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -96,8 +96,34 @@ export type UseInfiniteQueryMethod extends never - ? [InitWithUnknowns?, Omit>, "queryKey" | "queryFn">?, QueryClient?] - : [InitWithUnknowns, Omit>, "queryKey" | "queryFn">?, QueryClient?] + ? [ + 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> = < @@ -145,22 +171,30 @@ export default function createClient, - Init extends MaybeOptionalInit, - Response extends Required>, ->( - method: Method, - path: Path, - init?: InitWithUnknowns, - options?: Omit>, "queryKey" | "queryFn"> -) => ({ - queryKey: [method, path, init] as const, - queryFn, - ...options, -}); + Method extends HttpMethod, + Path extends PathsWithMethod, + 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, @@ -170,12 +204,15 @@ export default function createClient, 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); + 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(