Skip to content
Open
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 docs/.vitepress/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default defineConfig({
{ text: "useSuspenseQuery", link: "/use-suspense-query" },
{ text: "useInfiniteQuery", link: "/use-infinite-query" },
{ text: "queryOptions", link: "/query-options" },
{ text: "infiniteQueryOptions", link: '/infinite-query-options'}
],
},
{
Expand Down
161 changes: 161 additions & 0 deletions docs/openapi-react-query/infinite-query-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: infiniteQueryOptions
---

# {{ $frontmatter.title }}

The `infiniteQueryOptions` method allows you to construct type-safe [Infinite Query Options](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries).

`infiniteQueryOptions` can be used together with `@tanstack/react-query` APIs that take infinite query options, such as
[useInfiniteQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery),
[usePrefetchInfiniteQuery](https://tanstack.com/query/latest/docs/framework/react/reference/usePrefetchInfiniteQuery),
[QueryClient.fetchInfiniteQuery](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientfetchinfinitequery)
among many others.

If you would like to use an infinite query API that is not explicitly supported by `openapi-react-query`, this is the way to go.

## Examples

[useInfiniteQuery example](use-infinite-query#example) rewritten using `infiniteQueryOptions`.

::: code-group

```tsx [src/app.tsx]
import { useInfiniteQuery } from '@tanstack/react-query';
import { $api } from './api';

const PostList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery(
$api.infiniteQueryOptions(
'get',
'/posts',
{
params: {
query: {
limit: 10,
},
},
},
{
getNextPageParam: lastPage => lastPage.nextPage,
initialPageParam: 0,
}
)
);

return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.items.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetching}>
{isFetching ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
};
```

```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<paths>({
baseUrl: 'https://myapi.dev/v1/',
});
export const $api = createClient(fetchClient);
```

:::

::: info Good to Know

[useInfiniteQuery](use-infinite-query) uses `infiniteQueryOptions` under the hood.

:::

Usage with `usePrefetchInfiniteQuery`.

::: code-group

```tsx [src/post-list.tsx]
import { usePrefetchInfiniteQuery } from '@tanstack/react-query';
import { $api } from './api';

export const PostList = () => {
// Prefetch infinite query
usePrefetchInfiniteQuery(
$api.infiniteQueryOptions(
'get',
'/posts',
{
params: {
query: {
limit: 10,
},
},
},
{
getNextPageParam: lastPage => lastPage.nextPage,
initialPageParam: 0,
pages: 3, // Prefetch first 3 pages
}
)
);

// ... rest of the component
};
```

```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<paths>({
baseUrl: 'https://myapi.dev/v1/',
});
export const $api = createClient(fetchClient);
```

:::

## Api

```tsx
const infiniteQueryOptions = $api.infiniteQueryOptions(method, path, options, infiniteQueryOptions);
```

**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` **(required)**
- `pageParamName` The query param name used for pagination, `"cursor"` by default.
- `initialPageParam` **(required)**: The initial page param for the first page.
- `getNextPageParam`: Function to calculate the next page param.
- `getPreviousPageParam`: Function to calculate the previous page param.
- Additional infinite query options can be passed through.

**Returns**

- [Infinite Query Options](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries)
- Fully typed thus `data` and `error` will be correctly deduced.
- `queryKey` is `[method, path, params]`.
- `queryFn` is set to a fetcher function that handles pagination.
4 changes: 2 additions & 2 deletions packages/openapi-react-query/biome.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"root": false,
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"extends": "//",
"files": {
"includes": ["**", "!dist/**", "!test/fixtures/**"]
"includes": ["**", "!dist", "!test/fixtures"]
},
"linter": {
"rules": {
Expand Down
124 changes: 91 additions & 33 deletions packages/openapi-react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,59 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
}
>;

// Helper type to infer TPageParam type
type InferPageParamType<T> = T extends { initialPageParam: infer P } ? P : unknown;

export type InfiniteQueryOptionsFunction<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
Options extends Omit<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>,
"queryKey" | "queryFn"
> & {
pageParamName?: string;
initialPageParam: InferPageParamType<Options>;
},
>(
method: Method,
path: Path,
init: InitWithUnknowns<Init>,
options: Options,
) => NoInfer<
Omit<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>,
"queryFn"
> & {
queryFn: Exclude<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>["queryFn"],
SkipToken | undefined
>;
}
>;

export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Expand Down Expand Up @@ -112,11 +165,12 @@ export type UseInfiniteQueryMethod<Paths extends Record<string, Record<HttpMetho
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
unknown
InferPageParamType<Options>
>,
"queryKey" | "queryFn"
> & {
pageParamName?: string;
initialPageParam: InferPageParamType<Options>;
},
>(
method: Method,
Expand Down Expand Up @@ -166,6 +220,7 @@ export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}

export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
queryOptions: QueryOptionsFunction<Paths, Media>;
infiniteQueryOptions: InfiniteQueryOptionsFunction<Paths, Media>;
useQuery: UseQueryMethod<Paths, Media>;
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
Expand Down Expand Up @@ -214,44 +269,47 @@ export default function createClient<Paths extends {}, Media extends MediaType =
...options,
});

const infiniteQueryOptions: InfiniteQueryOptionsFunction<Paths, Media> = (method, path, init, options) => {
const { pageParamName = "cursor", initialPageParam, ...restOptions } = options;
const { queryKey } = queryOptions(method, path, init);

return {
queryKey,
initialPageParam,
queryFn: async ({ queryKey: [method, path, init], pageParam, signal }) => {
const mth = method.toUpperCase() as Uppercase<typeof method>;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const mergedInit = {
...init,
signal,
params: {
...(init?.params || {}),
query: {
...(init?.params as { query?: DefaultParamsOption })?.query,
[pageParamName]: pageParam,
},
},
};

const { data, error } = await fn(path, mergedInit as any);
if (error) {
throw error;
}
return data;
},
...restOptions,
};
};

return {
queryOptions,
infiniteQueryOptions,
useQuery: (method, path, ...[init, options, queryClient]) =>
useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
useInfiniteQuery: (method, path, init, options, queryClient) => {
const { pageParamName = "cursor", ...restOptions } = options;
const { queryKey } = queryOptions(method, path, init);
return useInfiniteQuery(
{
queryKey,
queryFn: async ({ queryKey: [method, path, init], pageParam = 0, signal }) => {
const mth = method.toUpperCase() as Uppercase<typeof method>;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const mergedInit = {
...init,
signal,
params: {
...(init?.params || {}),
query: {
...(init?.params as { query?: DefaultParamsOption })?.query,
[pageParamName]: pageParam,
},
},
};

const { data, error } = await fn(path, mergedInit as any);
if (error) {
throw error;
}
return data;
},
...restOptions,
},
queryClient,
);
},
useInfiniteQuery: (method, path, init, options, queryClient) =>
useInfiniteQuery(infiniteQueryOptions(method, path, init, options), queryClient),
useMutation: (method, path, options, queryClient) =>
useMutation(
{
Expand Down
Loading