Skip to content

Commit

Permalink
feat: Add useInfiniteQuery support (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
7nohe authored Aug 6, 2024
1 parent e87d22c commit e7626d9
Show file tree
Hide file tree
Showing 22 changed files with 630 additions and 54 deletions.
86 changes: 77 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Features

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

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

Expand Down Expand Up @@ -234,6 +236,72 @@ function App() {
export default App;
```

##### Using Infinite Query hooks

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.

Example Schema:

```yml
paths:
/paginated-pets:
get:
description: |
Returns paginated pets from the system that the user has access to
operationId: findPaginatedPets
parameters:
- name: page
in: query
description: page number
required: false
schema:
type: integer
format: int32
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
nextPage:
type: integer
format: int32
minimum: 1
```
Usage of Generated Hooks:
```ts
import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";

const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
limit: 10,
tags: [],
});
```

##### Runtime Configuration

You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
Expand Down
37 changes: 37 additions & 0 deletions examples/nextjs-app/app/components/PaginatedPets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import React from "react";

export default function PaginatedPets() {
const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
limit: 10,
tags: [],
});

return (
<>
<h1>Pet List with Pagination</h1>
<ul>
{data?.pages.map((group, i) => (
<React.Fragment key={group.pets?.at(0)?.id}>
{group.pets?.map((pet) => (
<li key={pet.id}>{pet.name}</li>
))}
</React.Fragment>
))}
</ul>
{data?.pages.at(-1)?.nextPage && (
<button
type="button"
onClick={() => fetchNextPage()}
className="bg-blue-500 px-4 py-2 text-white mt-4"
>
Load More
</button>
)}
<ReactQueryDevtools initialIsOpen={false} />
</>
);
}
File renamed without changes.
9 changes: 9 additions & 0 deletions examples/nextjs-app/app/infinite-loader/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PaginatedPets from "../components/PaginatedPets";

export default async function InfiniteLoaderPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<PaginatedPets />
</main>
);
}
6 changes: 5 additions & 1 deletion examples/nextjs-app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
QueryClient,
dehydrate,
} from "@tanstack/react-query";
import Pets from "./pets";
import Link from "next/link";
import Pets from "./components/Pets";

export default async function Home() {
const queryClient = new QueryClient();
Expand All @@ -19,6 +20,9 @@ export default async function Home() {
<HydrationBoundary state={dehydrate(queryClient)}>
<Pets />
</HydrationBoundary>
<Link href="/infinite-loader" className="underline">
Go to Infinite Loader &rarr;
</Link>
</main>
);
}
5 changes: 4 additions & 1 deletion examples/nextjs-app/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export const request = <T>(
url: options.url,
data: options.body,
method: options.method,
params: options.path,
params: {
...options.query,
...options.path,
},
headers: formattedHeaders,
cancelToken: source.token,
})
Expand Down
46 changes: 46 additions & 0 deletions examples/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,52 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/paginated-pets:
get:
description: |
Returns paginated pets from the system that the user has access to
operationId: findPaginatedPets
parameters:
- name: page
in: query
description: page number
required: false
schema:
type: integer
format: int32
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
nextPage:
type: integer
format: int32
minimum: 1

components:
schemas:
Pet:
Expand Down
5 changes: 4 additions & 1 deletion examples/react-app/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export const request = <T>(
url: options.url,
data: options.body,
method: options.method,
params: options.path,
params: {
...options.query,
...options.path,
},
headers: formattedHeaders,
cancelToken: source.token,
})
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/cli.mts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type LimitedUserConfig = {
debug?: boolean;
noSchemas?: boolean;
schemaType?: "form" | "json";
pageParam: string;
nextPageParam: string;
};

async function setupProgram() {
Expand Down Expand Up @@ -90,6 +92,16 @@ async function setupProgram() {
"Type of JSON schema [Default: 'json']",
).choices(["form", "json"]),
)
.option(
"--pageParam <value>",
"Name of the query parameter used for pagination",
"page",
)
.option(
"--nextPageParam <value>",
"Name of the response parameter used for next page",
"nextPage",
)
.parse();

const options = program.opts<LimitedUserConfig>();
Expand Down
1 change: 1 addition & 0 deletions src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const modalsFileName = "types.gen";

export const OpenApiRqFiles = {
queries: "queries",
infiniteQueries: "infiniteQueries",
common: "common",
suspense: "suspense",
index: "index",
Expand Down
19 changes: 17 additions & 2 deletions src/createExports.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type ts from "typescript";
import { createPrefetch } from "./createPrefetch.mjs";
import { createUseMutation } from "./createUseMutation.mjs";
import { createUseQuery } from "./createUseQuery.mjs";
import type { Service } from "./service.mjs";

export const createExports = (service: Service) => {
export const createExports = (
service: Service,
pageParam: string,
nextPageParam: string,
) => {
const { klasses } = service;
const methods = klasses.flatMap((k) => k.methods);

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

const allGetQueries = allGet.map((m) => createUseQuery(m));
const allGetQueries = allGet.map((m) =>
createUseQuery(m, pageParam, nextPageParam),
);
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));

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

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

const infiniteQueriesExports = allQueries
.flatMap(({ infiniteQueryHook }) => [infiniteQueryHook])
.filter(Boolean) as ts.VariableStatement[];

const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [
suspenseQueryHook,
]);
Expand All @@ -81,6 +92,10 @@ export const createExports = (service: Service) => {
* Main exports are the hooks that are used in the components
*/
mainExports,
/**
* Infinite queries exports are the hooks that are used in the infinite scroll components
*/
infiniteQueriesExports,
/**
* Suspense exports are the hooks that are used in the suspense components
*/
Expand Down
Loading

0 comments on commit e7626d9

Please sign in to comment.