T
TanStack3y ago
xenial-black

useQuery data is undefined even with initialData/placeholderData

Hey, i have a custom hook that returns a useQuery that fetches the options for a select input. I have tried with both initialData and placeholderData set as an empty array (which typewise is not incorrect, since I can have a select without options until they are added), but the data returned is set always set as <T | undefined> with T being the type of the options... Does it still require me to validate if isSuccess is true in order to set the type to T even though I have set the "default data"? - @tanstack/react-query v4.24.4 - typescript v4.9.5
19 Replies
xenial-black
xenial-black3y ago
can you show this in a sandbox please? not sure if you're talking about a runtime or a type-level problem... at runtime it should work in both cases on type-level, only initialData would narrow the type
xenial-black
xenial-blackOP3y ago
rfdomingues98
CodeSandbox
restless-mountain-h65p7z - CodeSandbox
restless-mountain-h65p7z by rfdomingues98 using @tanstack/react-query, @types/react, @types/react-dom, @types/uuid, @vitejs/plugin-react, autoprefixer, postcss, react, react-dom
xenial-black
xenial-blackOP3y ago
If you hover over the data variable, you see its type can be undefined even though i passed initialData to it
xenial-black
xenial-black3y ago
because you explicitly type as UseQueryResult<TechnologiesType["items"], Error>, thus taking away type inference that makes this feature possible 🤷 delete that and it works
xenial-black
xenial-blackOP3y ago
Wow... My manager told me to explicitly type everything... I told him it wasn't a good idea, and here's why... Thank you very much! And sorry for such dumb question 🤦‍♂️
xenial-black
xenial-black3y ago
this video is for your manager then. especially the last minutes where every TypeScript expert ever tells you to infer return types 😅 https://www.youtube.com/watch?v=I6V2FkW1ozQ
Theo - t3․gg
YouTube
The Dangers Of Return Types in TypeScript
I'm sorry Prime ❤️ #typescript THANK YOU TO ALL THE AWESOME GUESTS - Malte https://twitter.com/cramforce - Trash @trash_dev - Ben Holmes https://twitter.com/BHolmesDev/ - Josh Goldberg https://twitter.com/JoshuaKGoldberg - Dax Raad https://twitter.com/thdxr - Maple https://twitter.com/heyImMapleLeaf - Alex https://twitter.com/alexdotjs/ - Ta...
xenial-black
xenial-blackOP3y ago
Already had sent him that video, he agreed with some parts, disagreed with some other parts, but didn't really discuss with me anything about it... oh well, might change his mind with this situation
rival-black
rival-black3y ago
Sorry to kind of highjack the thread but I have a similar problem @TkDodo 🔮 is there a way to accept options as a function wrapper and keep the initialData nont undefined feature ? I tried to replicate the option type but when I pass the initialData to my abstraction via options, my data stays undefined
export const useTypedApiQuery = <T extends QueryDefinition>(
definition: T,
params?: Parameters<T>[0],
options?: Omit<
UseQueryOptions<Awaited<ReturnType<ReturnType<T>['queryFn']>>>,
'queryKey' | 'queryFn'
>
) => {
const store = useHydrate()
const { queryKey, queryFn } = getQuery(store)(definition, params)
return useQuery(queryKey, () => queryFn(), options)
}

const { data } = useTypedApiQuery(
^? still Data | undefined
myDefinition,
params,
{ initialData: {} }
)
export const useTypedApiQuery = <T extends QueryDefinition>(
definition: T,
params?: Parameters<T>[0],
options?: Omit<
UseQueryOptions<Awaited<ReturnType<ReturnType<T>['queryFn']>>>,
'queryKey' | 'queryFn'
>
) => {
const store = useHydrate()
const { queryKey, queryFn } = getQuery(store)(definition, params)
return useQuery(queryKey, () => queryFn(), options)
}

const { data } = useTypedApiQuery(
^? still Data | undefined
myDefinition,
params,
{ initialData: {} }
)
xenial-black
xenial-black3y ago
only with overloads like we do internally
like-gold
like-gold3y ago
We faced a similar issue in our codebase and what solved it for us is "does your hook actually need to accept every possible option that useQuery can accept?". For example, we were trying to have our custom hook accept the whole options object while, in practice, we were only ever passing onSuccess and onError. Refactoring the hook to directly accept onSuccess and onError instead of options allowed us to keep the type inference.
xenial-black
xenial-black3y ago
⬆️ this is the way. I always advise to not allow passing all options. like, would you really want to have different cacheTimes passed from consumers?
rival-black
rival-black3y ago
Yeah I'll do the same, indeed we don't use that many options too thx both of you !
eastern-cyan
eastern-cyan3y ago
@TkDodo 🔮 Hi, I kinda have the similar problem with initialData. So here's the example
// Custom hook
export function useAccount(uuid?: string, initialData?: IAccount) {
return useQuery({
queryKey: ['account', uuid],
queryFn: () => services.accounts.getByID(uuid).then((res) => res.data),
initialData,
enabled: !!uuid,
});
}

// Component that uses it
export default function Account({ id, accountData }: AccountProps) {
....
....
const account = useAccount(id, accountData);
....
....

if (account.isLoading) return null;
if (account.isError) return null;

return (
<Head>
// account.data is still undefined here even though I have a guard
<title>Account {account.data.name}</title>
</Head>
);
}
// Custom hook
export function useAccount(uuid?: string, initialData?: IAccount) {
return useQuery({
queryKey: ['account', uuid],
queryFn: () => services.accounts.getByID(uuid).then((res) => res.data),
initialData,
enabled: !!uuid,
});
}

// Component that uses it
export default function Account({ id, accountData }: AccountProps) {
....
....
const account = useAccount(id, accountData);
....
....

if (account.isLoading) return null;
if (account.isError) return null;

return (
<Head>
// account.data is still undefined here even though I have a guard
<title>Account {account.data.name}</title>
</Head>
);
}
To give You some context, I want useAccount to accept initialData but only in some cases, so I make it optional. QueryFn always return the IAccount type so the problem hides inside the initialData itself, when I remove it, or make it required my guard works just fine so the account.data is always defined, but with initialData being optional, everything falls apart. Do You have any idea how can I work around this problem?
like-gold
like-gold3y ago
What if you only pass initialData if it's actually present?
return useQuery({
queryKey: ['account', uuid],
queryFn: () => services.accounts.getByID(uuid).then((res) => res.data),
...(initialData && {initialData: initialData as IAccount}), // not sure if "as IAccount" is required
enabled: !!uuid,
});
return useQuery({
queryKey: ['account', uuid],
queryFn: () => services.accounts.getByID(uuid).then((res) => res.data),
...(initialData && {initialData: initialData as IAccount}), // not sure if "as IAccount" is required
enabled: !!uuid,
});
eastern-cyan
eastern-cyan3y ago
This doesn't help. From what I can tell the problem is with types, cause when initialData is present, useQuery actually doesn't go through loading state, it treats initialData as actual data in a fresh state. That's why we don't have hard loading state. So frankly speaking types don't lie, if I check account.isLoading it doesn't guard from data being undefined. The only work around I have found so far is to check if(account.data) instead of if(account.isLoading). @TkDodo 🔮 I'm just curious if I get this problem right and if there's a better way to guard against this type of situations?!
xenial-black
xenial-black3y ago
Can you show a typescript playground reproduction please?
eastern-cyan
eastern-cyan3y ago
nice-bash-vf5j7t
CodeSandbox is an online editor tailored for web applications.
xenial-black
xenial-black3y ago
the initialData you pass to useQuery is of type IAccount | undefined, hence we cannot narrow the type. If you want to replicate the behaviour that we do on your end, you either need conditional return types, or overloads (we use overloads) or, you make two hooks: useAccount and useAcountWithInitialData, where initialData is not undefined
like-gold
like-gold3y ago
I might be missing something but what I suggested above seems to remove the error?
export function useAccount(id?: number, initialData?: IAccount) {
return useQuery({
queryKey: ["account", id],
queryFn: () => getAccount(id),
// Try to remove initialData completley, make it required,
// or initialData: undefined and error will disappear
...(initialData && { initialData }),
enabled: !!id,
});
}
export function useAccount(id?: number, initialData?: IAccount) {
return useQuery({
queryKey: ["account", id],
queryFn: () => getAccount(id),
// Try to remove initialData completley, make it required,
// or initialData: undefined and error will disappear
...(initialData && { initialData }),
enabled: !!id,
});
}
https://codesandbox.io/p/sandbox/nice-bash-vf5j7t?file=%2FREADME.md
nice-bash-vf5j7t
CodeSandbox is an online editor tailored for web applications.

Did you find this page helpful?