Skip to content

Commit e7626d9

Browse files
authored
feat: Add useInfiniteQuery support (#122)
1 parent e87d22c commit e7626d9

22 files changed

+630
-54
lines changed

README.md

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Features
88

9-
- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery` and `useMutation` hooks
9+
- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks
1010
- Generates query keys and functions for query caching
1111
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
1212

@@ -45,18 +45,20 @@ Options:
4545
-V, --version output the version number
4646
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
4747
-o, --output <value> Output directory (default: "openapi")
48-
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
48+
-c, --client <value> HTTP client to generate (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch")
4949
--request <value> Path to custom request file
50-
--format <value> Process output folder with formatter? ['biome', 'prettier']
51-
--lint <value> Process output folder with linter? ['eslint', 'biome']
50+
--format <value> Process output folder with formatter? (choices: "biome", "prettier")
51+
--lint <value> Process output folder with linter? (choices: "biome", "eslint")
5252
--operationId Use operation ID to generate operation names?
53-
--serviceResponse <value> Define shape of returned value from service calls ['body', 'response'] (default: "body")
53+
--serviceResponse <value> Define shape of returned value from service calls (choices: "body", "response", default: "body")
5454
--base <value> Manually set base in OpenAPI config instead of inferring from server value
55-
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
55+
--enums <value> Generate JavaScript objects from enum definitions? (choices: "javascript", "typescript")
5656
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
57-
--debug Enable debug mode
58-
--noSchemas Disable generating schemas for request and response objects
59-
--schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
57+
--debug Run in debug mode?
58+
--noSchemas Disable generating JSON schemas
59+
--schemaType <value> Type of JSON schema [Default: 'json'] (choices: "form", "json")
60+
--pageParam <value> Name of the query parameter used for pagination (default: "page")
61+
--nextPageParam <value> Name of the response parameter used for next page (default: "nextPage")
6062
-h, --help display help for command
6163
```
6264

@@ -234,6 +236,72 @@ function App() {
234236
export default App;
235237
```
236238

239+
##### Using Infinite Query hooks
240+
241+
This feature will generate a function in infiniteQueries.ts when the name specified by the `pageParam` option exists in the query parameters and the name specified by the `nextPageParam` option exists in the response.
242+
243+
Example Schema:
244+
245+
```yml
246+
paths:
247+
/paginated-pets:
248+
get:
249+
description: |
250+
Returns paginated pets from the system that the user has access to
251+
operationId: findPaginatedPets
252+
parameters:
253+
- name: page
254+
in: query
255+
description: page number
256+
required: false
257+
schema:
258+
type: integer
259+
format: int32
260+
- name: tags
261+
in: query
262+
description: tags to filter by
263+
required: false
264+
style: form
265+
schema:
266+
type: array
267+
items:
268+
type: string
269+
- name: limit
270+
in: query
271+
description: maximum number of results to return
272+
required: false
273+
schema:
274+
type: integer
275+
format: int32
276+
responses:
277+
'200':
278+
description: pet response
279+
content:
280+
application/json:
281+
schema:
282+
type: object
283+
properties:
284+
pets:
285+
type: array
286+
items:
287+
$ref: '#/components/schemas/Pet'
288+
nextPage:
289+
type: integer
290+
format: int32
291+
minimum: 1
292+
```
293+
294+
Usage of Generated Hooks:
295+
296+
```ts
297+
import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";
298+
299+
const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
300+
limit: 10,
301+
tags: [],
302+
});
303+
```
304+
237305
##### Runtime Configuration
238306

239307
You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";
4+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5+
import React from "react";
6+
7+
export default function PaginatedPets() {
8+
const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
9+
limit: 10,
10+
tags: [],
11+
});
12+
13+
return (
14+
<>
15+
<h1>Pet List with Pagination</h1>
16+
<ul>
17+
{data?.pages.map((group, i) => (
18+
<React.Fragment key={group.pets?.at(0)?.id}>
19+
{group.pets?.map((pet) => (
20+
<li key={pet.id}>{pet.name}</li>
21+
))}
22+
</React.Fragment>
23+
))}
24+
</ul>
25+
{data?.pages.at(-1)?.nextPage && (
26+
<button
27+
type="button"
28+
onClick={() => fetchNextPage()}
29+
className="bg-blue-500 px-4 py-2 text-white mt-4"
30+
>
31+
Load More
32+
</button>
33+
)}
34+
<ReactQueryDevtools initialIsOpen={false} />
35+
</>
36+
);
37+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import PaginatedPets from "../components/PaginatedPets";
2+
3+
export default async function InfiniteLoaderPage() {
4+
return (
5+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
6+
<PaginatedPets />
7+
</main>
8+
);
9+
}

examples/nextjs-app/app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
QueryClient,
55
dehydrate,
66
} from "@tanstack/react-query";
7-
import Pets from "./pets";
7+
import Link from "next/link";
8+
import Pets from "./components/Pets";
89

910
export default async function Home() {
1011
const queryClient = new QueryClient();
@@ -19,6 +20,9 @@ export default async function Home() {
1920
<HydrationBoundary state={dehydrate(queryClient)}>
2021
<Pets />
2122
</HydrationBoundary>
23+
<Link href="/infinite-loader" className="underline">
24+
Go to Infinite Loader &rarr;
25+
</Link>
2226
</main>
2327
);
2428
}

examples/nextjs-app/request.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export const request = <T>(
7777
url: options.url,
7878
data: options.body,
7979
method: options.method,
80-
params: options.path,
80+
params: {
81+
...options.query,
82+
...options.path,
83+
},
8184
headers: formattedHeaders,
8285
cancelToken: source.token,
8386
})

examples/petstore.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,52 @@ paths:
135135
application/json:
136136
schema:
137137
$ref: '#/components/schemas/Error'
138+
/paginated-pets:
139+
get:
140+
description: |
141+
Returns paginated pets from the system that the user has access to
142+
operationId: findPaginatedPets
143+
parameters:
144+
- name: page
145+
in: query
146+
description: page number
147+
required: false
148+
schema:
149+
type: integer
150+
format: int32
151+
- name: tags
152+
in: query
153+
description: tags to filter by
154+
required: false
155+
style: form
156+
schema:
157+
type: array
158+
items:
159+
type: string
160+
- name: limit
161+
in: query
162+
description: maximum number of results to return
163+
required: false
164+
schema:
165+
type: integer
166+
format: int32
167+
responses:
168+
'200':
169+
description: pet response
170+
content:
171+
application/json:
172+
schema:
173+
type: object
174+
properties:
175+
pets:
176+
type: array
177+
items:
178+
$ref: '#/components/schemas/Pet'
179+
nextPage:
180+
type: integer
181+
format: int32
182+
minimum: 1
183+
138184
components:
139185
schemas:
140186
Pet:

examples/react-app/request.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export const request = <T>(
7777
url: options.url,
7878
data: options.body,
7979
method: options.method,
80-
params: options.path,
80+
params: {
81+
...options.query,
82+
...options.path,
83+
},
8184
headers: formattedHeaders,
8285
cancelToken: source.token,
8386
})

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export type LimitedUserConfig = {
2323
debug?: boolean;
2424
noSchemas?: boolean;
2525
schemaType?: "form" | "json";
26+
pageParam: string;
27+
nextPageParam: string;
2628
};
2729

2830
async function setupProgram() {
@@ -90,6 +92,16 @@ async function setupProgram() {
9092
"Type of JSON schema [Default: 'json']",
9193
).choices(["form", "json"]),
9294
)
95+
.option(
96+
"--pageParam <value>",
97+
"Name of the query parameter used for pagination",
98+
"page",
99+
)
100+
.option(
101+
"--nextPageParam <value>",
102+
"Name of the response parameter used for next page",
103+
"nextPage",
104+
)
93105
.parse();
94106

95107
const options = program.opts<LimitedUserConfig>();

src/constants.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const modalsFileName = "types.gen";
77

88
export const OpenApiRqFiles = {
99
queries: "queries",
10+
infiniteQueries: "infiniteQueries",
1011
common: "common",
1112
suspense: "suspense",
1213
index: "index",

src/createExports.mts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import type ts from "typescript";
12
import { createPrefetch } from "./createPrefetch.mjs";
23
import { createUseMutation } from "./createUseMutation.mjs";
34
import { createUseQuery } from "./createUseQuery.mjs";
45
import type { Service } from "./service.mjs";
56

6-
export const createExports = (service: Service) => {
7+
export const createExports = (
8+
service: Service,
9+
pageParam: string,
10+
nextPageParam: string,
11+
) => {
712
const { klasses } = service;
813
const methods = klasses.flatMap((k) => k.methods);
914

@@ -23,7 +28,9 @@ export const createExports = (service: Service) => {
2328
m.httpMethodName.toUpperCase().includes("DELETE"),
2429
);
2530

26-
const allGetQueries = allGet.map((m) => createUseQuery(m));
31+
const allGetQueries = allGet.map((m) =>
32+
createUseQuery(m, pageParam, nextPageParam),
33+
);
2734
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
2835

2936
const allPostMutations = allPost.map((m) => createUseMutation(m));
@@ -60,6 +67,10 @@ export const createExports = (service: Service) => {
6067

6168
const mainExports = [...mainQueries, ...mainMutations];
6269

70+
const infiniteQueriesExports = allQueries
71+
.flatMap(({ infiniteQueryHook }) => [infiniteQueryHook])
72+
.filter(Boolean) as ts.VariableStatement[];
73+
6374
const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [
6475
suspenseQueryHook,
6576
]);
@@ -81,6 +92,10 @@ export const createExports = (service: Service) => {
8192
* Main exports are the hooks that are used in the components
8293
*/
8394
mainExports,
95+
/**
96+
* Infinite queries exports are the hooks that are used in the infinite scroll components
97+
*/
98+
infiniteQueriesExports,
8499
/**
85100
* Suspense exports are the hooks that are used in the suspense components
86101
*/

0 commit comments

Comments
 (0)