lukemorales / query-key-factory

A library for creating typesafe standardized query keys, useful for cache management in @tanstack/query
https://www.npmjs.com/package/@lukemorales/query-key-factory
MIT License
1.15k stars 31 forks source link

Add helpers for extracting variables / options #48

Open JanStevens opened 1 year ago

JanStevens commented 1 year ago

Hi,

Typically we use react-query in the following way, by writing a small reusable hook, ex:

// Example of request vars
type EventRequestVariables {
  count: number;
  filter: Record<string, string>,
  skip: number;
}

// in queries.ts
export const EventsQueries = createQueryKeys('events', {
  // bunch of other events related queries
  events: (variables: EventRequestVariables) => ({
    queryKey: [variables],
    queryFn: () => api.events(variables),
  }),
})

// in useEvents.ts
export const useEvents = (
  variables?: QueryVariables<typeof EventsQueries.events>,
  options?: CommonQueryOptions<typeof EventsQueries.events>,
) => {
  const { locale } = useLocale();

  const { data, status, isLoading, isError, refetch } = useQuery({
    ...EventsQueries.events({ locale, ...variables }),
    staleTime: StaleTime.FOREVER,
    ...options,
  });

  return {
    data: data?.allPageEvents || [],
    status,
    isLoading,
    isError,
    refetch,
  };
};

This allows us to easily expose what we need from useQuery while allowing to reuse useEvents across our application. I've used 2 helper types to extract the variables and query options from the QueryKey factory instance.

They could use some love but I think it might be a great addition to this library, also potentially solving issue #40 without making the typings less strict.

// We need to use any here for generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type GenericFunction<T = any> = (...args: any[]) => T;

// Extracts the query function
type ExtractQueryFn<T> = T extends GenericFunction<{
  queryFn: infer QueryFn;
}>
  ? QueryFn extends GenericFunction
    ? QueryFn
    : never
  : never;

// Extracts the queryData aka the returned Data
type ExtractQueryData<T> = T extends GenericFunction<{
  queryFn: infer QueryFn;
}>
  ? QueryFn extends GenericFunction
    ? Awaited<ReturnType<QueryFn>>
    : never
  : never;

// Extract the query key
type ExtractQueryKey<T> = T extends GenericFunction<{
  queryKey: infer TQueryKey;
}>
  ? TQueryKey extends QueryKey
    ? TQueryKey
    : never
  : never;

// You can use this to allow passing in custom variables
export type QueryVariables<
  T extends GenericFunction,
> = Parameters<T>[0];

// You can use this to pass correct Query options, allowing to pass a custom error
export type CommonQueryOptions<
  T extends GenericFunction,
  TError = Error,
  TQueryData = ExtractQueryData<T>,
  TQueryKey extends QueryKey = ExtractQueryKey<T>,
> = UseQueryOptions<TQueryData, TError, TQueryData, TQueryKey>;

Right now it always assumes the Keys are a function, I couldn't really manage to wrap my head around the different options like passing null or returning a string

WDYT?

joeyfigaro commented 1 month ago

Is this discussion still open?

@JanStevens did you end up using this in any of your work? I'm currently having issues with query function return values if they return a promise. Looks like TS doesn't like something about GenericFunction<any>.