Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RTK-Q: Types for custom createApi with custom conditional hooks #1072

Closed
mtodberg opened this issue May 19, 2021 · 9 comments
Closed

RTK-Q: Types for custom createApi with custom conditional hooks #1072

mtodberg opened this issue May 19, 2021 · 9 comments

Comments

@mtodberg
Copy link

mtodberg commented May 19, 2021

I am trying to create a custom createApi as per the documentation example on the website, https://deploy-preview-1016--redux-starter-kit-docs.netlify.app/usage/rtk-query/customizing-create-api. Besides the regular modules, coreModule and reactHooksModule, my custom api will be adding a module for creating paginated hooks. I need to do this as the built-in pagination example on the website is too simple for my use-case. My paginated hook needs to support infinite scrolling, next/prev methods and more. Also what the REST API will be returning is quite different as it may contain lots of query parameters and not just the simple page number.

Basically I want to be able to define a paginated endpoint like this:

getPokemons: builder.query<
      GetPokemonsResponse,
      GetPokemonsRequest
    >({
      query: () => ({
        version: 3,
        paginated: true, // declared either here
      }),
      extraOptions: {
        paginated: true, // or declared here
      },
    }),

This will then be the createApi definition:

export const createApi = buildCreateApi(
  coreModule(),
  reactHooksModule(),
  reactPaginatedHooksModule(),
);

The reactPaginatedHooksModule will be defined as:

const reactPaginatedHooksModuleName = Symbol(); 
type ReactPaginatedHooksModule = typeof reactPaginatedHooksModuleName;

const reactPaginatedHooksModule = (): Module<ReactPaginatedHooksModule> => ({ ... })

I am then mimicking what is being done in the source-code of your reactHooksModule:

declare module '@rtk-incubator/rtk-query/react' {
  export interface ApiModules<
    // eslint-disable-next-line @typescript-eslint/naming-convention
    BaseQuery extends BaseQueryFn,
    // eslint-disable-next-line @typescript-eslint/naming-convention
    Definitions extends EndpointDefinitions,
    // eslint-disable-next-line @typescript-eslint/naming-convention
    ReducerPath extends string,
    // eslint-disable-next-line @typescript-eslint/naming-convention
    TagTypes extends string
  > {
    [reactPaginatedHooksModuleName]: TS41Hooks<Definitions>;
  }
}

export type TS41Hooks<
  TDefinitions extends EndpointDefinitions
> = keyof TDefinitions extends infer Keys
  ? Keys extends string
    ? TDefinitions[Keys]['extraOptions'] extends { paginated: true } // extraOptions is `unknown` here :(
      ? {
          [K in Keys as `usePaginated${Capitalize<K>}Query`]: UsePaginatedQuery<
            Extract<TDefinitions[K], QueryDefinition<any, any, any, any>>
          >;
        }
      : never
    : never
  : never;

And now the problem:
The problem is that I cannot get the typings correct so that usePaginated<endpoint>Query as a type on the api will be available when extraOptions: { paginated: true } is being passed. I think the problem is because the extraOptions is unknown and therefore gets lost in type inference. I need a way to type the extra options.

@phryneas
Copy link
Member

phryneas commented May 19, 2021

Couldn't you just do

TDefinitions[Keys] extends { extraOptions: { paginated: true } }

?

Other than that: if you provide a playground or codesandbox that can showcase all of that, I can try to play around a bit with it, but generally we have all hands full to do preparing the release right now, so I cannot promise too much, sorry.

@mtodberg
Copy link
Author

mtodberg commented May 20, 2021

I realised what I was trying to do is technically impossible as the interface declaration merging going on with ApiModules in my paginated module, has no awareness of the actual apis created with createApi and what is being passed in extraOptions. It seems to work in your case because extends { type: DefinitionType.query } is checking up against the EndpointDefinition type unions for QueryEndpointDefinition and MutationEndpointDefinition respectively.

So I'll rephrase my question instead: How do you make more advanced paginated queries with your library when you have the following requirements:

  • Make the first call with a bunch of parameters.
  • Make subsequent calls using the next/prev urls that you obtain in the payload of the previous responses.
  • Support infinite scrolling (by appending/prepending to list).

@phryneas
Copy link
Member

I honestly wouldn't overthink it too much.
Choose a page size big enough to cover the user's screen and have three useXQuery calls in there, one for the current page, one for the previous page and one for the next page. Skip the previous/next if no information is available for them.
Combine the results of those in the component.

Stuff that has not been scrolled back to in a while will be collected from the cache, stuff that already is in the cache will be retrieved almost instantly.

@mtodberg
Copy link
Author

Doing it in the component is definitely also an option, however, I would have to repeat this pattern over and over for every component that uses a paginated query. This in turn bloats the components, which is why I want to encapsulate the behaviour.

@phryneas
Copy link
Member

phryneas commented May 20, 2021

Doing it in component means "write it once, build a custom hook around it".

Could look like usePagination(api.endpoints.X, otherArgs) in your abstraction in the end.

And if you want to share that hook in the end - all the better. We still need documentation on infinite scrolling. No solution from one person will be applicable to another, but some (potenially simplified) examples as starting points for different scenarios would be great in there.

@mtodberg
Copy link
Author

I've thought about doing it like that, but then the problem becomes the types for api.endpoints.X as this type doesn't seem to be exported anywhere. This is basically what stopped me from going down that path.

usePagination(api.endpoints.X, otherArgs)

export function usePagination<TArgs, TResponse>(endpoint: ???, args: TArgs) {...}

I could also pass in the action creator from api.endpoints.X.initiate, but the library also doesn't expose the StartQueryActionCreator and QueryDefinition which are part of the initiate type.

StartQueryActionCreator<QueryDefinition<GetPokemonsRequest, AxiosBaseQueryFn, never, GetPokemonsResponse, string, Record<string, any>>>

@phryneas
Copy link
Member

The "what types we expose" is certainly one of the last things we need to go through when releasing. For now it should be sufficient to import the types from dist, which should definitely be possible.

@mtodberg
Copy link
Author

Created a custom hook and it works great. I'm importing the types from dist for now.

@nikischin
Copy link

nikischin commented Dec 12, 2023

Thank you so much @mtodberg, I am building a custom useAutoSaveMutationHook autogenerated within createApi and this samples you provided saved my day. I had the implementation working already, though I was really struggling a lot getting the types correct, so that the auto-suggestion would work. Without your samples I would have never managed to get this working hah although still having some ts errors not yet resolved but at least auto-suggesting the endpoint now does it's job.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants