React-query: TypeScript: Not possible to pass the type of the QueryKey to `useQuery`(react-query v3.3.0)

Created on 17 Dec 2020  路  9Comments  路  Source: tannerlinsley/react-query

Describe the bug
Current typings of useQuery (also prefetchQuery and fetchQuery) support string-only query keys.
It seems not possible to pass a custom type (for array keys).

To Reproduce
https://codesandbox.io/s/objective-nobel-8incc?file=/src/App.tsx

Expected behavior
It should be possible to pass a generic type for query keys.

Desktop

  • Version: 3.3.0

Most helpful comment

Good morning. Thank you @TkDodo for pointing again to the inline function workaround. I just tested this in our codebase and it is obviously working. I think during the update I got a little bit lost with all the type warning / errors. I have to say I like the inline function way a little more than my proposed solution.

Cheers. 馃憢

All 9 comments

not sure what you mean. You can pass anything serializable as a queryKey. Have you read the migration guide, especially:

https://react-query.tanstack.com/guides/migrating-to-react-query-3#query-key-partspieces-are-no-longer-automatically-spread-to-the-query-function

react-query will no longer "inject" the queryKey to your fetchFn. It is recommended to use inline functions, especially with typeScript to get typesafety:

const fetchContentById = async (id: number) => ...
...
const id = 1;
const query = useQuery(["contentById", id], () => fetchContentById(id));

Hi @TkDodo , thanks for your answer.

image

This is basically what I want to achieve with TypeScript:
https://react-query.tanstack.com/guides/query-functions#query-function-variables

Ah yes, I think you are right. I haven't actually used the context before, the types are defined like this:

export declare type QueryKey = string | unknown[];
export declare type QueryFunction<T = unknown> = (context: QueryFunctionContext) => T | Promise<T>;
export interface QueryFunctionContext<TQueryKey extends QueryKey = QueryKey, TPageParam = any> {
    queryKey: TQueryKey;
    pageParam?: TPageParam;
}

so the QueryFunction only takes a QueryFunctionContext without generic, I think we might need to change that to:

export declare type QueryKey = string | unknown[];
- export declare type QueryFunction<T = unknown> = (context: QueryFunctionContext) => T | Promise<T>;
+ export declare type QueryFunction<T = unknown, TQueryKey extends QueryKey = QueryKey> = (context: QueryFunctionContext<QueryKey>) => T | Promise<T>;
export interface QueryFunctionContext<TQueryKey extends QueryKey = QueryKey, TPageParam = any> {
    queryKey: TQueryKey;
    pageParam?: TPageParam;
}

I cannot change the type declarations in codesandbox to try if this actually works, maybe you can try it out?

Thanks, this change allows to pass the QueryKey type to QueryFunction.
But useQuery does not allow it:

https://codesandbox.io/s/xenodochial-burnell-fds01?file=/src/App.tsx

image

I guess there's the same issue with prefetchQuery and fetchQuery.

I run today into the same problem during the migration from v2 to v3. I guess the examples in the documentation assume that you use the useQuery hook along with the queryFn in one file. For us this is not the case as we have the useQuery hook in another place than the queryFn. This let us not simply use a variable inside the function, rather we need to pass the QueryFunctionContext and destruct it manually which is working fine without TS. I got it now also working with the TS compiler in the following way:

/**
 * This is in a dir called hooks e.g.
 * hooks/userChangeLogQuery.ts
 */
import { useQuery, UseQueryResult } from 'react-query';
import {
  queryChangeLog,
  QueriedChangeLog,
} from '@ui/reactQuery/queryFunctions/queryChangeLog';
import { GraphQLResponse } from '@ui/axios/requestGraphQL';

export const userChangeLogQuery = (
  id: string
): UseQueryResult<GraphQLResponse<QueriedChangeLog>, Error> =>
  useQuery(
    ['CHANGELOG_QUERY', { id }],
    queryChangeLog
  );

and here is the corresponding query function:

/**
 * This is in a dir called queryFunction e.g.
 * queryFunction/queryChangeLog.ts
 */
import { QueryFunctionContext } from 'react-query';
import { requestGraphQL, GraphQLResponse } from '@ui/axios/axios/requestGraphQL';
import { ChangeLog } from '@server/src/graphql/types/generated';

export type QueriedChangeLog = {
  getChangeLog: ChangeLog[];
};


export const queryChangeLog = async (
  context: QueryFunctionContext
): Promise<GraphQLResponse<QueriedChangeLog>> => {
 // If I would not do the type assertion here type it would work with our compiler settings.
 const [, { id }] = context.queryKey as [string, { id: string }];
  return requestGraphQL<QueriedChangeLog>(
    /* GraphQL */ `
      query getChangeLog($id: ID!) {
        getChangeLog(id: $id) {
          date_of_change
          field
          old_value
          new_value
          comment
        }
      }
    `,
    {
      id
    }
  );
}

I guess if I would just use the id without wrapping it into an object I could maybe access the array inside the queryChangeLog function. An update of the documentation on this topic would be awesome. If you want I can try to dig into it and propose an PR. Also some hint if I'm on the right track would be awesome and also sorry for the long example.

Cheers 馃憢

I've made an attempt to make QueryKey generic in the codebase but with no success 馃槗
Tried to start small but inevitably ended up modifying lots of files as it's (QueryKey) used pretty much everywhere, and there are also lots of any that I needed to get rid of but failed to. e.g. https://github.com/tannerlinsley/react-query/blob/80cecef22c3e088d6cd9f8fbc5cd9e2c0aab962f/src/core/utils.ts#L91

I stopped because it led me nowhere, plus I don't know if it was the right thing to do though.

Anyway, for now I've used the workaround from @Kiesen 馃憤
e.g.

const getContentByIdQueryKey = (id: string) => {
  return ['contentById', id];
};

const fetchContentById = async ({ queryKey }: QueryFunctionContext) => {
  const [key, id] = queryKey as ReturnType<typeof getContentByIdQueryKey>;
  // fetch data
  return data;
};

export const useContentById = (id: string) => {
  return useQuery(getContentByIdQueryKey(id), fetchContentById);
};

So why is this workaround better than using inline functions?

const getContentByIdQueryKey = (id: string) => {
  return ['contentById', id];
};

const fetchContentById = async (id: string) => {
  // fetch data
  return data;
};

export const useContentById = (id: string) => {
  return useQuery(getContentByIdQueryKey(id), () => fetchContentById(id));
};

it should be possible to always do that, because if you can build the query key, you can also pass the same arguments to your fetch function. everything that you need in the fetch function is now an explicit dependency :)

Hi @TkDodo,
I think it's just a matter of taste ;)
When using the QueryFunctionContext with a custom type, you get the intellisense and autocomplete for free when destructuring the key 馃槢
Anyway I'm good with the 2 solutions, I think we can close this issue 馃憤

Good morning. Thank you @TkDodo for pointing again to the inline function workaround. I just tested this in our codebase and it is obviously working. I think during the update I got a little bit lost with all the type warning / errors. I have to say I like the inline function way a little more than my proposed solution.

Cheers. 馃憢

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ivowork picture ivowork  路  5Comments

the133448 picture the133448  路  4Comments

tiagofernandez picture tiagofernandez  路  3Comments

alveshelio picture alveshelio  路  6Comments

oukayuka picture oukayuka  路  4Comments