redux-toolkit
redux-toolkit copied to clipboard
Update infinite query draft PR to separate `PageParams` type
This PR is a copy of the "Infinite Query API" draft PR in #4393 , but with (currently) one additional commit that tries to modify the infinite query endpoint definitions to add a separate PageParam TS generic and pass it through:
- https://github.com/reduxjs/redux-toolkit/commit/3f0b35fe933c663508295f6a7708307d95ffbd93
Background
With the current implementation in #4393, the QueryArg type gets used as the type for the pageParam field. That results in an awkward usage pattern, because there's a conflation between "the arg we use to generate the cache key" vs "the value we need to define the next page to fetch".
What I think we want is to specify builder.infiniteQuery<ReturnType, QueryArg, PageParam>(). What we should end up with is a cached data structure of {pages: ReturnType[], pageParams: PageParam[]}.
We also should then override the normal query endpoint types so that we end up with query: (arg: PageParam), but keep the external behavior of initiate(arg: QueryArg).
I could be wrong on all this, but this seems like it makes sense to my admittedly-confused brain.
Current WIP
In https://github.com/reduxjs/redux-toolkit/commit/3f0b35fe933c663508295f6a7708307d95ffbd93 , I've successfully threaded a PageParam generic through all the layers of our endpoint definitions. Where I'm stuck is getting that split behavior between the type of arg in initiate() vs the type of arg in query().
In infiniteQueries.test.tsx, I have this example code:
const pokemonApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
// GOAL: Specify both the query arg (for cache key serialization)
// and the page param type (for feeding into the query URL)
getInfinitePokemon: builder.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
getNextPageParam: (
lastPage,
allPages,
// ✅Currently: page param type is `number`
lastPageParam,
allPageParams,
) => lastPageParam + 1,
},
// ❌ This seems to be controlled by `BaseEndpointDefinition`
// GOAL: should be `pageParam: number`
query(pageParam) {
return `https://example.com/listItems?page=${pageParam}`
},
}),
}),
})
const res = storeRef.store.dispatch(
// ❌ This seems to be controlled by `BaseEndpointDefinition`.
// GOAL: should be `arg: string`
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}),
)
It looks like the behavior is ultimately controlled by our BaseEndpointDefinition type. The existing implementation is:
// ❌Existing logic:
// - ✅ `initiate(arg: string)
// - ❌ `query(pageParam: string)
export type BaseEndpointDefinition<
QueryArg,
BaseQuery extends BaseQueryFn,
ResultType,
> = (
| ([CastAny<BaseQueryResult<BaseQuery>, {}>] extends [NEVER]
? never
: EndpointDefinitionWithQuery<QueryArg, BaseQuery, ResultType>)
| EndpointDefinitionWithQueryFn<QueryArg, BaseQuery, ResultType>
)
I've tried three other variations, each of which fails in a different way:
// GOAL: if `PageParam` is supplied, then we should
// pass it through so that it becomes the argument type
// for `query` and `queryFn`.
// Otherwise, we stick with `QueryArg`.
// However, `initiate` should always receive`QueryArg`.
// ❌ Failing change 1: use `FinalQueryArg`:
// - ❌ `initiate(arg: number)
// - ✅`query(pageParam: number)
export type BaseEndpointDefinition<
QueryArg,
BaseQuery extends BaseQueryFn,
ResultType,
PageParam = never,
FinalQueryArg = [PageParam] extends [never] ? QueryArg : PageParam,
> = (
| ([CastAny<BaseQueryResult<BaseQuery>, {}>] extends [NEVER]
? never
: EndpointDefinitionWithQuery<FinalQueryArg, BaseQuery, ResultType>)
| EndpointDefinitionWithQueryFn<FinalQueryArg, BaseQuery, ResultType>
// ❌ Failing change 2: one nested `PageParam` checks:
// - ❌ `initiate(arg: string | number)
// - ✅ `query(pageParam: number)`
export type BaseEndpointDefinition<
QueryArg,
BaseQuery extends BaseQueryFn,
ResultType,
PageParam = never,
> = (
| ([CastAny<BaseQueryResult<BaseQuery>, {}>] extends [NEVER]
? never
: [PageParam] extends [never]
? EndpointDefinitionWithQuery<QueryArg, BaseQuery, ResultType>
: EndpointDefinitionWithQuery<PageParam, BaseQuery, ResultType>)
: EndpointDefinitionWithQuery<QueryArg, BaseQuery, ResultType>
// ❌ Failing change 3: both nested `PageParam` checks:
// - ❌ `initiate(arg: unknown)
// - ❌ `query(pageParam: any)`, field is `undefined`
export type BaseEndpointDefinition<
QueryArg,
BaseQuery extends BaseQueryFn,
ResultType,
PageParam = never,
> = (
| ([CastAny<BaseQueryResult<BaseQuery>, {}>] extends [NEVER]
? never
: [PageParam] extends [never]
? EndpointDefinitionWithQuery<QueryArg, BaseQuery, ResultType>
: EndpointDefinitionWithQuery<PageParam, BaseQuery, ResultType>)
: [PageParam] extends [never]
? EndpointDefinitionWithQuery<QueryArg, BaseQuery, ResultType>
: EndpointDefinitionWithQuery<PageParam, BaseQuery, ResultType>
(I had to reconstruct those types by hand in this PR description - I probably made a typo somewhere, but hopefully it's clear what I'm going for)