Skip to content
Draft
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
27 changes: 27 additions & 0 deletions .changeset/suspense-infinite-query-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@kubb/plugin-react-query": minor
---

Add support for `useSuspenseInfiniteQuery` hook generation

This release adds support for generating `useSuspenseInfiniteQuery` hooks, enabling React Suspense with infinite queries in TanStack Query v5+.

**New Features:**
- Generate `useSuspenseInfiniteQuery` hooks when both `suspense` and `infinite` options are enabled
- Support for both cursor-based and offset-based pagination
- Full TypeScript type safety with proper generics
- Automatic validation of required query parameters and response fields

**Usage:**
```typescript
pluginReactQuery({
suspense: {},
infinite: {
queryParam: 'pageSize',
initialPageParam: 0,
cursorParam: 'nextCursor', // optional
},
})
```

This will generate hooks like `useFindPetsByTagsSuspenseInfinite` alongside the existing query hooks.
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ title: Changelog

# Changelog

# 4.6.0
- [`plugin-react-query`](/plugins/plugin-react-query/): Add support for `useSuspenseInfiniteQuery` hook generation
- Generate `useSuspenseInfiniteQuery` hooks when both `suspense` and `infinite` options are enabled
- Support for both cursor-based and offset-based pagination with full TypeScript type safety
- Automatic validation of required query parameters and response fields
- Example: `useFindPetsByTagsSuspenseInfinite` for React Suspense with infinite queries

# 4.5.13
- [`plugin-client`](/plugins/plugin-client): Type 'FormData' is missing the following properties from type at generated hooks

Expand Down
212 changes: 212 additions & 0 deletions packages/plugin-react-query/src/components/SuspenseInfiniteQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { isOptional, type Operation } from '@kubb/oas'
import type { OperationSchemas } from '@kubb/plugin-oas'
import { getComments, getPathParams } from '@kubb/plugin-oas/utils'
import { File, Function, FunctionParams } from '@kubb/react-fabric'
import type { KubbNode } from '@kubb/react-fabric/types'
import type { Infinite, PluginReactQuery } from '../types.ts'
import { QueryKey } from './QueryKey.tsx'
import { QueryOptions } from './QueryOptions.tsx'

type Props = {
/**
* Name of the function
*/
name: string
queryOptionsName: string
queryKeyName: string
queryKeyTypeName: string
typeSchemas: OperationSchemas
operation: Operation
paramsCasing: PluginReactQuery['resolvedOptions']['paramsCasing']
paramsType: PluginReactQuery['resolvedOptions']['paramsType']
pathParamsType: PluginReactQuery['resolvedOptions']['pathParamsType']
dataReturnType: PluginReactQuery['resolvedOptions']['client']['dataReturnType']
initialPageParam: Infinite['initialPageParam']
queryParam?: Infinite['queryParam']
}

type GetParamsProps = {
paramsType: PluginReactQuery['resolvedOptions']['paramsType']
paramsCasing: PluginReactQuery['resolvedOptions']['paramsCasing']
pathParamsType: PluginReactQuery['resolvedOptions']['pathParamsType']
typeSchemas: OperationSchemas
pageParamGeneric: string
}

function getParams({ paramsType, paramsCasing, pathParamsType, typeSchemas, pageParamGeneric }: GetParamsProps) {
if (paramsType === 'object') {
return FunctionParams.factory({
data: {
mode: 'object',
children: {
...getPathParams(typeSchemas.pathParams, {
typed: true,
casing: paramsCasing,
}),
data: typeSchemas.request?.name
? {
type: typeSchemas.request?.name,
optional: isOptional(typeSchemas.request?.schema),
}
: undefined,
params: typeSchemas.queryParams?.name
? {
type: typeSchemas.queryParams?.name,
optional: isOptional(typeSchemas.queryParams?.schema),
}
: undefined,
headers: typeSchemas.headerParams?.name
? {
type: typeSchemas.headerParams?.name,
optional: isOptional(typeSchemas.headerParams?.schema),
}
: undefined,
},
},
options: {
type: `
{
query?: Partial<UseSuspenseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, ${pageParamGeneric}>> & { client?: QueryClient },
client?: ${typeSchemas.request?.name ? `Partial<RequestConfig<${typeSchemas.request?.name}>> & { client?: typeof fetch }` : 'Partial<RequestConfig> & { client?: typeof fetch }'}
}
`,
default: '{}',
},
})
}

return FunctionParams.factory({
pathParams: typeSchemas.pathParams?.name
? {
mode: pathParamsType === 'object' ? 'object' : 'inlineSpread',
children: getPathParams(typeSchemas.pathParams, {
typed: true,
casing: paramsCasing,
}),
optional: isOptional(typeSchemas.pathParams?.schema),
}
: undefined,
data: typeSchemas.request?.name
? {
type: typeSchemas.request?.name,
optional: isOptional(typeSchemas.request?.schema),
}
: undefined,
params: typeSchemas.queryParams?.name
? {
type: typeSchemas.queryParams?.name,
optional: isOptional(typeSchemas.queryParams?.schema),
}
: undefined,
headers: typeSchemas.headerParams?.name
? {
type: typeSchemas.headerParams?.name,
optional: isOptional(typeSchemas.headerParams?.schema),
}
: undefined,
options: {
type: `
{
query?: Partial<UseSuspenseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, ${pageParamGeneric}>> & { client?: QueryClient },
client?: ${typeSchemas.request?.name ? `Partial<RequestConfig<${typeSchemas.request?.name}>> & { client?: typeof fetch }` : 'Partial<RequestConfig> & { client?: typeof fetch }'}
}
`,
default: '{}',
},
})
}

export function SuspenseInfiniteQuery({
name,
queryKeyTypeName,
queryOptionsName,
queryKeyName,
paramsType,
paramsCasing,
pathParamsType,
dataReturnType,
typeSchemas,
operation,
initialPageParam,
queryParam,
}: Props): KubbNode {
const responseType = dataReturnType === 'data' ? typeSchemas.response.name : `ResponseConfig<${typeSchemas.response.name}>`
const errorType = `ResponseErrorConfig<${typeSchemas.errors?.map((item) => item.name).join(' | ') || 'Error'}>`
const isInitialPageParamDefined = initialPageParam !== undefined && initialPageParam !== null
const fallbackPageParamType =
typeof initialPageParam === 'number'
? 'number'
: typeof initialPageParam === 'string'
? initialPageParam.includes(' as ')
? (() => {
const parts = initialPageParam.split(' as ')
return parts[parts.length - 1] ?? 'unknown'
})()
: 'string'
: typeof initialPageParam === 'boolean'
? 'boolean'
: 'unknown'
const queryParamType = queryParam && typeSchemas.queryParams?.name ? `${typeSchemas.queryParams?.name}['${queryParam}']` : undefined
const pageParamType = queryParamType ? (isInitialPageParamDefined ? `NonNullable<${queryParamType}>` : queryParamType) : fallbackPageParamType
const returnType = 'UseSuspenseInfiniteQueryResult<TData, TError> & { queryKey: TQueryKey }'
const generics = [
`TQueryFnData = ${responseType}`,
`TError = ${errorType}`,
'TData = InfiniteData<TQueryFnData>',
`TQueryKey extends QueryKey = ${queryKeyTypeName}`,
`TPageParam = ${pageParamType}`,
]

const queryKeyParams = QueryKey.getParams({
pathParamsType,
typeSchemas,
paramsCasing,
})
const queryOptionsParams = QueryOptions.getParams({
paramsType,
pathParamsType,
typeSchemas,
paramsCasing,
})
const params = getParams({
paramsCasing,
paramsType,
pathParamsType,
typeSchemas,
pageParamGeneric: 'TPageParam',
})

const queryOptions = `${queryOptionsName}(${queryOptionsParams.toCall()})`

return (
<File.Source name={name} isExportable isIndexable>
<Function
name={name}
export
generics={generics.join(', ')}
params={params.toConstructor()}
JSDoc={{
comments: getComments(operation),
}}
>
{`
const { query: queryConfig = {}, client: config = {} } = options ?? {}
const { client: queryClient, ...queryOptions } = queryConfig
const queryKey = queryOptions?.queryKey ?? ${queryKeyName}(${queryKeyParams.toCall()})

const query = useSuspenseInfiniteQuery({
...${queryOptions},
queryKey,
...queryOptions
} as unknown as UseSuspenseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient) as ${returnType}

query.queryKey = queryKey as TQueryKey

return query
`}
</Function>
</File.Source>
)
}

SuspenseInfiniteQuery.getParams = getParams
Loading
Loading