DigitalCanvasJourney

Abstract space drawing, 4 planets

Let's authorize your react queries!

And practice some fancy typescript while we're at it!

Published on 2025-04-01

Last Updated on 2025-04-13

You might think of adding it to the query keys, which is a simple way of passing this information to your fetcher, but it has a fundamental flaw: Adding a token as part of query key will make it reactively dependant on that token, it will cause all your queries to refetch whenever the token changes. This is not ideal, especially if your environment security demands a short-lived token.

Instead of adding access token to query keys, we can add a token to the fetcher itself. T.K.Dodo suggested a great way of doing this in their tweet.

Tweet by TKDodo

This approach yields quite a few benefits, like:

  • Loading the token and potential errors that can happen during that will be a part of our query state
  • We can cache the token and reloading will be triggered only once one of the queries needs to execute

Here's the full source code plus some extra generic types for convenience.


export type BaseAuthQueryKey = [string, Record<string, unknown>?];

export type AuthQueryOptions<
  TQueryKey extends BaseAuthQueryKey,
  TQueryFnData,
  TError,
  TData = TQueryFnData
> = Omit<
  UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  "queryKey" | "queryFn"
>;

export const useAuthQuery = <
  TQueryKey extends BaseAuthQueryKey,
  TQueryFnData,
  TError,
  TData = TQueryFnData
>(
  queryKey: TQueryKey,
  fetcher: (params: TQueryKey[1], token: string) => Promise<TQueryFnData>,
  options?: AuthQueryOptions<TQueryKey, TQueryFnData, TError, TData>
) => {
  const { getAccessToken } = useAuth();

  return useQuery({
    queryKey,
    queryFn: async () => {
      const token = await getAccessToken();
      return fetcher(queryKey[1], token);
    },
    ...options,
  });
};

Paying close attention to the code suggested above, we can spot that this implementation forces us to use a certain convention in our query keys:

  • Second key in the query key array needs to be a record that we use to pass everything we need to the fetcher
  • The first key in the query key array is a string that we use to identify the query
  • The fetcher function expects to always receive the second key as the first argument, the token as second

And this is how you can define custom hooks that extend this and are fully type-safe:


import { BaseAuthQueryKey, useAuthSuspenseQuery } from "./use-auth-query";

type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

const fetchPosts = async (_: unknown, token: string): Promise<Post[]> => {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  // 💡 Remove this if to check how the UI looks when query breaks
  if (!res.ok) {
    throw new Error("Failed to fetch posts");
  }

  return res.json();
};

export const usePosts = () =>
  useAuthSuspenseQuery<BaseAuthQueryKey, Post[], Error>(
    ["posts", undefined],
    fetchPosts
  );

Extra bits

Check out the full code in this example repo I built for this article:

react-query-access-token example

The repo contains a full setup for testing the code above, using it in components, handling errors and loading states etc etc.