Search code examples
typescriptreact-querytrpc.iot3

Passing a react-query trpc query as a hook parameter


I'd like to make a generic hook that takes useQuery as a parameter and handles pagination.

That means my trpc query will always provide a skip and a take parameter. And it will always return a count in the response and an array of items.

function usePagination(query){
  const [skip, take] = useSomethingThatManagesThis();

  const response = query.useQuery({
    skip,
    take,
  });
  
  return useMemo(() => ({
    items: response.data?.items,
    count: response.data?.count
  }), [response]);
}

function MyComponent() {
  const paginated = usePagination(api.user.list);
  
  const firstUserEmail = paginated.items?.[0].email;
}

It works in vanilla javascript, but I'm struggling to type it correctly.

I tried to naively reuse trpc client types, but I'm not able to distinguish request type (take, query) and response type (items).

import {
  AnyProcedure,
  AnyQueryProcedure,
  inferProcedureOutput,
} from "@trpc/server";

interface BaseInput {
  skip: number;
  take: number;
}

type UseDatatableArgs<TData extends AnyQueryProcedure> = {
  query?: {
    useQuery: (input: BaseInput) => ProcedureUseQuery<
      { count: number } & AnyProcedure,
      string
    > & {
      data: {
        count: number;
        items: inferProcedureOutput<TData>;
      };
    };
  };
};

function usePagination<TData extends AnyQueryProcedure>(query){
  const [skip, take] = useSomethingThatManagesThis();

  const response = query.useQuery({
    skip,
    take,
  });
  
  return useMemo(() => ({
    items: response.data?.items,
    count: response.data?.count
  }), [response]);
}

But I'm having no items typing in the response

function MyComponent() {
  const paginated = usePagination(api.user.list);
  
  const firstUserEmail = paginated.items?.[0].email; // paginated.items is any
}

I'm currently having a workaround by using types inferring utilities provided by TRPC, but I'd love to deduct that from the query

export type RouterOutputs = inferRouterOutputs<AppRouter>;

function MyComponent() {
  const paginated = usePagination<RouterOutputs["user"]["list"]>(api.user.list);

  const firstUserEmail = paginated.items?.[0].email;
}


Solution

  • You can make use of TypeScript's generic type inference and conditional types. import ProcedureType from @trpc server as well

    Here ProcedureType type represents the type of your TRPC procedure (query, mutation, etc.). The UseDatatableArgs type will take a generic parameter TData representing the type of the procedure, and it expects a query property that has a useQuery function accepting BaseInput as input and returning a ProcedureUseQuery type with the response type { count: number } & TData. Here's an edited copy of your code

    import { ProcedureType } from "@trpc/server";
    import { ProcedureUseQuery } from "trpc/react";
    
    interface BaseInput {
      skip: number;
      take: number;
    }
    
    type UseDatatableArgs<TData extends ProcedureType> = {
      query?: {
        useQuery: (input: BaseInput) => ProcedureUseQuery<
          { count: number } & TData,
          string
        >;
      };
    };
    
    type PaginationResponse<TData extends ProcedureType> = {
      items: TData extends { data: { items: infer Items } } ? Items : never;
      count: number;
    };
    
    function usePagination<TData extends ProcedureType>(
      query: UseDatatableArgs<TData>["query"]
    ) {
      const [skip, take] = useSomethingThatManagesThis();
    
      const response = query?.useQuery({
        skip,
        take,
      });
    
      return useMemo(() => ({
        items: response?.data?.items,
        count: response?.data?.count
      }), [response]);
    }