T
TanStack3y ago
ambitious-aqua

custom useQuery

Hey guys, ive been working on this and stumping myself for actually 8 hours today. I figured it would be pretty easy to create a useAuthenticatedQuery hook to simply respond with the access token in the queryFn, but with typescript it has been a total nightmare. Any help would be appreciated
const {accessToken} = useAuth()
const subscriptionQuery = useQuery({
queryKey: ["subscription", accessToken],
queryFn: () => {
return api.getSubscription(accessToken);
},
});
const {accessToken} = useAuth()
const subscriptionQuery = useQuery({
queryKey: ["subscription", accessToken],
queryFn: () => {
return api.getSubscription(accessToken);
},
});
This is the default query that im using out of the box in all of my screens. I dont want to add the access token to the key every time manually, and I dont want to call the useAuth hook for the access token every time. this code is close to what i want to accomplish (roughly) but typescript has made it hell.
const useAuthenticatedQuery = (options: UseQueryOptions) => {
const { accessToken } = useAuth();

return useQuery({
...options,
queryKey: [options.queryKey, accessToken],
queryFn: (context) => {
return options && options.queryFn && options.queryFn({ ...context, accessToken });
},
});
};
const useAuthenticatedQuery = (options: UseQueryOptions) => {
const { accessToken } = useAuth();

return useQuery({
...options,
queryKey: [options.queryKey, accessToken],
queryFn: (context) => {
return options && options.queryFn && options.queryFn({ ...context, accessToken });
},
});
};
I understand that i could not attach the access token as context, and rather call context.queryKey and get it from that array, but I cannot add the access token as a query key without typescript throwing a fit. (err below)
No overload matches this call.
The last overload gave the following error.
Argument of type '{ queryKey: (string | QueryKey | undefined)[]; queryFn: (context: any) => unknown; context?: Context<QueryClient | undefined> | undefined; enabled?: boolean | undefined; ... 31 more ...; meta?: QueryMeta | undefined; }' is not assignable to parameter of type 'QueryKey'.
Object literal may only specify known properties, and 'queryKey' does not exist in type 'readonly unknown[]'.ts(2769)
No overload matches this call.
The last overload gave the following error.
Argument of type '{ queryKey: (string | QueryKey | undefined)[]; queryFn: (context: any) => unknown; context?: Context<QueryClient | undefined> | undefined; enabled?: boolean | undefined; ... 31 more ...; meta?: QueryMeta | undefined; }' is not assignable to parameter of type 'QueryKey'.
Object literal may only specify known properties, and 'queryKey' does not exist in type 'readonly unknown[]'.ts(2769)
Any help is appreciated
5 Replies
ambitious-aqua
ambitious-aquaOP3y ago
This is the closest ive gotten
const useAuthenticatedQuery = <
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>(
key: TQueryKey,
fn: (accessToken: string) => QueryFunction<TQueryFnData, TQueryKey>,
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> = {}
) => {
const { accessToken } = useAuth();

return useQuery(key, fn(accessToken), {
...options,

onError: (error) => {
options.onError?.(error);
},
});
};
const useAuthenticatedQuery = <
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>(
key: TQueryKey,
fn: (accessToken: string) => QueryFunction<TQueryFnData, TQueryKey>,
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> = {}
) => {
const { accessToken } = useAuth();

return useQuery(key, fn(accessToken), {
...options,

onError: (error) => {
options.onError?.(error);
},
});
};
but that requires the function to look like this when called
const subscriptionQuery = useAuthenticatedQuery({
["subscription"],
() => (accessToken) => api.getSubscription(accessToken),
{},
})
const subscriptionQuery = useAuthenticatedQuery({
["subscription"],
() => (accessToken) => api.getSubscription(accessToken),
{},
})
Which is not only ugly, but also still doesnt solve the problem of the access token in the keys, and im posituive theres a better way
extended-salmon
extended-salmon3y ago
Have you considered using request interceptors to achieve this? It'll remove the bearer token concern from your wrapper hooks entirely
ambitious-aqua
ambitious-aquaOP3y ago
I was going to, but then I wouldnt be able to invalidate queries on logout right? however, I did come up with a solution i believe, if you wouldnt mind taking a look to see if this seems proper
import { AxiosError } from "axios";

type MyQueryKey = [string, QueryKey];

const useAuthenticatedQuery = <
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = MyQueryKey
>(
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
) => {
const { accessToken, fetchAccessToken } = useAuth();

const queryKey = [accessToken, options.queryKey] as unknown as TQueryKey;

return useQuery({
...options,
queryKey: queryKey,
retry: (failureCount, error) => {
if (failureCount > 2) {
return false;
}

if (!(error instanceof AxiosError)) {
return false;
}

if (error.response?.status != 401) {
return false;
}

if (error.response?.data?.message === "EXPIRED_TOKEN") {
// fetch access token and retry
fetchAccessToken();

return true;
}

return false;
},
});
};

export default useAuthenticatedQuery;
import { AxiosError } from "axios";

type MyQueryKey = [string, QueryKey];

const useAuthenticatedQuery = <
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = MyQueryKey
>(
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
) => {
const { accessToken, fetchAccessToken } = useAuth();

const queryKey = [accessToken, options.queryKey] as unknown as TQueryKey;

return useQuery({
...options,
queryKey: queryKey,
retry: (failureCount, error) => {
if (failureCount > 2) {
return false;
}

if (!(error instanceof AxiosError)) {
return false;
}

if (error.response?.status != 401) {
return false;
}

if (error.response?.data?.message === "EXPIRED_TOKEN") {
// fetch access token and retry
fetchAccessToken();

return true;
}

return false;
},
});
};

export default useAuthenticatedQuery;
const subscriptionQuery = useAuthenticatedQuery({
queryKey: ["subscription"],
queryFn: ({ queryKey }) => {
const [accessToken] = queryKey;

return api.getSubscription(accessToken);
},
});
const subscriptionQuery = useAuthenticatedQuery({
queryKey: ["subscription"],
queryFn: ({ queryKey }) => {
const [accessToken] = queryKey;

return api.getSubscription(accessToken);
},
});
extended-salmon
extended-salmon3y ago
You would be able to. You can do this imperatively via methods on the query client
ambitious-aqua
ambitious-aquaOP3y ago
oh yeah i forgot about that, good point

Did you find this page helpful?