T
TanStack3y ago
ratty-blush

Typescript - pass queryOptions in custom hook to useQuery

How are we supposed to take useQuery's query options as parameter to our custom hooks and pass it down while maintaining proper typescript type information?
16 Replies
ratty-blush
ratty-blushOP3y ago
I've tried with:
export const useData = (options?: QueryHookParams) => {
return useQuery({
queryKey: DATA_KEYS.ALL,
queryFn: () => DataService.getData(),
...options
});
};
export const useData = (options?: QueryHookParams) => {
return useQuery({
queryKey: DATA_KEYS.ALL,
queryFn: () => DataService.getData(),
...options
});
};
Where QueryHookParams = export type QueryHookParams = Omit<UseQueryOptions, 'queryKey' | 'queryFn'>;; However, this has a few issues. First, typescript is confused as it considers the passed object as QueryKey type and not query options thus resulting in an error. I circumvent that by doing:
export const useData = (options?: QueryHookParams) => {
const queryOptions: UseQueryOptions = {
queryKey: DATA_KEYS.ALL,
queryFn: () => DataService.getData(),
...options
};

return useQuery(queryOptions);
};
export const useData = (options?: QueryHookParams) => {
const queryOptions: UseQueryOptions = {
queryKey: DATA_KEYS.ALL,
queryFn: () => DataService.getData(),
...options
};

return useQuery(queryOptions);
};
which is not ideal. Second, as I haven't provided the types for the generics, when trying to use the hook like: useCurrencies(); the returned type is unknown instead of the properly infered type.
national-gold
national-gold3y ago
This is something I've been wondering about as well. Currently, I'm typing the options as UseQueryOptions<ReturnType,any,ReturnType,any> and it does the trick. It does feel very boilerplaty to do it like this and I wonder if it couldn't be inferred from the return type of the queryFn in any way.
ratty-blush
ratty-blushOP3y ago
Yeah, it is quite frustrating. I don't know, this is such a trivial and general case, I don't know why there aren't any examples of it. How are we supposed to pass the parameters in the first place? One should be able to just spread the incoming props over the useQuery but the type gets misinterpreted and doesn't allow it. @TkDodo 🔮 So sorry for tagging you but can you shed some light on the case, please?
flat-fuchsia
flat-fuchsia3y ago
It does allow it, but my take is that you likely don't really want to build a custom hook that allows to pass in all options. It's not a good abstraction, especially not for the example you've shown. If you want it, your custom hook needs 4 generics.
ratty-blush
ratty-blushOP3y ago
If it is not too insolent of me, can you please show me a working example? I really just want to forward the properties, nothing more. I am not trying to make an abstraction. Returning query object (observer) kills all possibility of abstractions anyway.
flat-fuchsia
flat-fuchsia3y ago
I'm explaining here how to do it and also why you don't want to do it 🙂 https://twitter.com/TkDodo/status/1491451513264574501
Dominik 🇺🇦 (@TkDodo)
I have been asked a lot lately how to make your own low-level abstraction over useQuery and have it work in #TypeScript. My answer is usually: You don't need it, as those abstractions are often too wide. But there are use-cases for it, so here is my take. Let's break it down ⬇️
Likes
267
From Dominik 🇺🇦 (@TkDodo)
Twitter
ratty-blush
ratty-blushOP3y ago
Thank you, I am on it.
flat-fuchsia
flat-fuchsia3y ago
I'd really like to know why you need it, in your example, where you have a fixed queryFn and queryKey. What's the point of allowing all options like cacheTime to be passed by consumers? You say you "don't want to create an abstraction", but that's what custom hooks are ... my guess is you think you need it but you actually don't 🙂
ratty-blush
ratty-blushOP3y ago
Yes, I don't need all of them, that's true. I need enabled, the refetch* flags, the retry flags, the callbacks and select. I have different refetch needs for the same data. For example, if I present the data in a table I want the refetch on focus enabled. However, when I use the data for a dropdown picker in a form, I don't want it to get refreshed on window focus. Same underlying data, different needs for it. Thank you for the input, I appreciate it. @TkDodo 🔮 Hey, I followed the example in the twitter post you provided. Can you, please, provide an example on how to build a concrete hook on top of it (like usePosts), though? No example was given in the twitter post about that but it is exactly where the question arises. If I have:
export const useQueryHook = <
TQueryKey extends QueryKey,
TQueryFnData,
TError,
TData = TQueryFnData
>(
queryKey: TQueryKey,
fetcher: (queryKey: TQueryKey) => Promise<TQueryFnData>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey,
queryFn: () => fetcher(queryKey),
...options
});
}
export const useQueryHook = <
TQueryKey extends QueryKey,
TQueryFnData,
TError,
TData = TQueryFnData
>(
queryKey: TQueryKey,
fetcher: (queryKey: TQueryKey) => Promise<TQueryFnData>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey,
queryFn: () => fetcher(queryKey),
...options
});
}
I guess I should:
export const useCurrencies = (options?: QueryHookParams<ICurrency[], unknown, ICurrency[], typeof CURRENCIES_QUERY_KEYS.ALL>) => {
return useQueryHook(
CURRENCIES_QUERY_KEYS.ALL,
() => CurrenciesService.getCurrencies(),
options
);
};
export const useCurrencies = (options?: QueryHookParams<ICurrency[], unknown, ICurrency[], typeof CURRENCIES_QUERY_KEYS.ALL>) => {
return useQueryHook(
CURRENCIES_QUERY_KEYS.ALL,
() => CurrenciesService.getCurrencies(),
options
);
};
? Where QueryHookParams is:
export type QueryHookParams<TQueryFnData, TError, TData, TQueryKey extends QueryKey> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryKey' | 'queryFn'>;
export type QueryHookParams<TQueryFnData, TError, TData, TQueryKey extends QueryKey> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryKey' | 'queryFn'>;
And I should specify the generic types on each and every custom hook I make? Also, I am not sure what type I should pass for TQueryKey as obviously typeof CURRENCIES_QUERY_KEYS.ALL is a pretty bad idea but it is the only thing that actually gets accepted by typescript. Using unknown[] / any[] results in:
Argument of type 'readonly ["currencies"]' is not assignable to parameter of type 'any[]'.
The type 'readonly ["currencies"]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
Argument of type 'readonly ["currencies"]' is not assignable to parameter of type 'any[]'.
The type 'readonly ["currencies"]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
CURRENCIES_QUERY_KEYS is defined as:
const CURRENCIES_KEY = 'currencies';

export const CURRENCIES_QUERY_KEYS = {
ALL: [CURRENCIES_KEY] as const
};
const CURRENCIES_KEY = 'currencies';

export const CURRENCIES_QUERY_KEYS = {
ALL: [CURRENCIES_KEY] as const
};
flat-fuchsia
flat-fuchsia3y ago
Show a typescript playground please
ratty-blush
ratty-blushOP3y ago
@TkDodo 🔮 Yes, of course.
ratty-blush
ratty-blushOP3y ago
zhulien-ivanov
CodeSandbox
suspicious-dubinsky-2zyd97 - CodeSandbox
suspicious-dubinsky-2zyd97 by zhulien-ivanov using @tanstack/react-query, react, react-dom, react-scripts
ratty-blush
ratty-blushOP3y ago
options?: QueryHookParams<ICurrency[], unknown, ICurrency[], unknown[]> is the culprit. unknown[] \ any[] doesn't work for me. Only any does. Thank you once again for taking the time.
flat-fuchsia
flat-fuchsia3y ago
TkDodo
CodeSandbox
silly-tdd-wiby85 - CodeSandbox
silly-tdd-wiby85 by TkDodo using @tanstack/react-query, react, react-dom, react-scripts
ratty-blush
ratty-blushOP3y ago
This is exactly how I did it before that. Not sure why I specify the type or why I introduce the useQueryHook hook in the first place?
flat-fuchsia
flat-fuchsia3y ago
not sure why there are two abstractions, useQueryHook and useCurrencies that are both super generic and need to pass-through all options

Did you find this page helpful?