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-18

Last Updated on 2025-05-28

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, useAuthQuery } 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 = () =>
    useAuthQuery<BaseAuthQueryKey, Post[], Error>(
      ["posts", undefined],
      fetchPosts
    );

Testing our code

Entire setup for testing react apps is out of scope of this article, so let's assume that we already have a working setup with following test dependencies installed:

First we setup a mock server that will intercept the request in the usePosts hook and return a mock response.


  // src/mocks/server.ts

  import { http, HttpResponse } from 'msw';
  import { setupServer } from 'msw/node';

  export const MockServerData = {
    posts: [
      { id: 1, title: 'Post 1', content: 'Content of post 1' },
      { id: 2, title: 'Post 2', content: 'Content of post 2' },
    ],
  };

  export const server = setupServer(
    http.get(/.*/posts/gi, () => {
      return HttpResponse.json(MockServerData.posts, {
        status: 200,
      });
    })
  );

Now to the test of the usePosts hook itself!


  // src/hooks/use-posts.test.tsx
  // ...import statements

  // 🛠️ Using mock service worker we set up above
  import { MockServerData, server } from "@/mocks/server";

  // 🚨 This mocks all imports of this module,
  // even the ones that happen before this line.
  // This will get hoisted to the top of this file as such,
  // usePosts hook that imports `useAuthQuery` that imports
  // `useAuth` will be mocked as well!
  vi.mock("@/context/auth-context");

  const mocks = vi.hoisted(() => ({
    getAccessToken: vi.fn(),
  }));

  // 🛠️ .mocked is but a handy typescript utility, doesn't make useAuth a mock
  // simply helps with types
  // we took care of mocking useAuth with vi.mock above
  vi.mocked(useAuth).mockReturnValue({
    getAccessToken: mocks.getAccessToken,
  });

Test cases themselves are pretty straightforward, we use the renderHook utility from @testing-library/react-hooks. We mock getAccessToken to throw or return successfully, and assert expected hook state.


  // src/hooks/use-posts.test.tsx
  // ...code above

  describe("usePosts", () => {
    // 🛠️ start server before all test cases, stop it after all test cases
    // reset handlers after each test case
    // this way we can set up custom handlers for each test case
    beforeAll(() => server.listen());

    afterEach(() => {
      server.resetHandlers();

      // 🛠️ cleans out mock state, but doesn't affect implementation
      // this way we don't loose the return value,
      // configured by the .mocked.mockReturnValue above
      vi.clearAllMocks();
    });
    afterAll(() => server.close());

    it("should return data when the query is successful", async () => {
      mocks.getAccessToken.mockResolvedValueOnce("mocked-access-token");
      const { result } = renderHook(() => usePosts());

      // 🛠️ declarative way of making the test wait for the query to finish
      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      expect(mocks.getAccessToken).toHaveBeenCalledTimes(1);
      expect(result.current.data).toEqual(MockServerData.posts);
    });

    it("should return error if failed to acquire token", async () => {
      const error = new Error("Failed to acquire token");
      mocks.getAccessToken.mockRejectedValueOnce(error);
      const { result } = renderHook(() => usePosts());

      expect(mocks.getAccessToken).toHaveBeenCalledTimes(1);
      await waitFor(() => {
        expect(result.current.error).toBe(error);
      });
    });
  });

Checkout the complete test file.

Thanks for reading!

I hope this helped you understand how to add access token to all your queries in a type-safe way and how to test it. If you have any questions, feel free to open an issue in the example repo!