Testing setState from async functions inside hook’s useEffect

I’m writing a hook that uses configcat to fetch a feature flag value for my react-native application.

The hook I drafted looks like this:

import * as configcat from "configcat-js";
import { useEffect, useMemo, useState } from "react";

type FeatureFlagHookResult = [boolean | undefined, boolean, unknown];

/**
 * Hook that returns the value of a feature flag from ConfigCat
 * @param key The key of the feature flag
 * @returns A tuple containing the value of the feature flag, a boolean indicating if the value is ready, and an error if one occurred
 */
export function useFeatureFlag(key: string): FeatureFlagHookResult {
  const [value, setValue] = useState<boolean>();
  const [isReady, setIsReady] = useState(false);
  const [error, setError] = useState<unknown>(null);

  const client = useMemo(() => {
    if (!process.env.EXPO_PUBLIC_CONFIGCAT_SDK_KEY) {
      throw new Error(
        "ConfigCat SDK key is missing. Add EXPO_PUBLIC_CONFIGCAT_SDK_KEY to the .env file.",
      );
    }
    const logger = __DEV__
      ? configcat.createConsoleLogger(configcat.LogLevel.Info)
      : undefined;

    return configcat.getClient(
      process.env.EXPO_PUBLIC_CONFIGCAT_SDK_KEY,
      configcat.PollingMode.AutoPoll,
      {
        logger,
      },
    );
  }, []);

  useEffect(() => {
    async function fetchValue() {
      try {
        const value = await client.getValueAsync(key, false);
        setValue(value);
      } catch (e) {
        setError(e);
      } finally {
        setIsReady(true);
      }
    }
    fetchValue();
  }, [client, key]);

  return [value, isReady, error];
}

the corresponding test like this:

import { renderHook, waitFor } from "@testing-library/react-native";
import * as configcat from "configcat-js";

import { useFeatureFlag } from "./useFeatureFlag";

describe("useFeatureFlag", () => {
  it("should return the value of a feature flag from ConfigCat", async () => {
    const mockGetValueAsync = jest.fn().mockResolvedValue(true);
    const mockClient = {
      getValueAsync: mockGetValueAsync,
    };
    jest.spyOn(configcat, "getClient").mockReturnValue(mockClient as any);

    const { result } = renderHook(() => useFeatureFlag("key"));

    expect(result.current).toEqual([undefined, false, null]);

    await waitFor(() => {
      expect(result.current).toEqual([true, true, null]);
      expect(mockGetValueAsync).toHaveBeenCalledWith("key", false);
    });
  });

  it("should return an error if one occurred", async () => {
    const mockGetValueAsync = jest.fn().mockRejectedValue(new Error("error"));
    const mockClient = {
      getValueAsync: mockGetValueAsync,
    };
    jest.spyOn(configcat, "getClient").mockReturnValue(mockClient as any);

    const { result } = renderHook(() => useFeatureFlag("key"));

    expect(result.current).toEqual([undefined, false, null]);

    await waitFor(() => {
      expect(result.current).toEqual([undefined, true, new Error("error")]);
      expect(mockGetValueAsync).toHaveBeenCalledWith("key", false);
    });
  });
});

given the code works well, the test fails due to some issues setting isReady flag:

 FAIL  src/configcat/useFeatureFlag.test.ts
  useFeatureFlag
    ✕ should return the value of a feature flag from ConfigCat (13 ms)
    ✕ should return an error if one occurred (6 ms)

  ● useFeatureFlag › should return the value of a feature flag from ConfigCat

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 1

      Array [
        true,
    -   true,
    +   false,
        null,
      ]

      16 |     expect(result.current).toEqual([undefined, false, null]);
      17 |
    > 18 |     await waitFor(() => {
         |                  ^
      19 |       expect(result.current).toEqual([true, true, null]);
      20 |       expect(mockGetValueAsync).toHaveBeenCalledWith("key", false);
      21 |     });

      at Object.<anonymous> (src/configcat/useFeatureFlag.test.ts:18:18)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
      at node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:7
      at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:14:12)

  ● useFeatureFlag › should return an error if one occurred

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 1

      Array [
        undefined,
    -   true,
    +   false,
        [Error: error],
      ]

      33 |     expect(result.current).toEqual([undefined, false, null]);
      34 |
    > 35 |     await waitFor(() => {
         |                  ^
      36 |       expect(result.current).toEqual([undefined, true, new Error("error")]);
      37 |       expect(mockGetValueAsync).toHaveBeenCalledWith("key", false);
      38 |     });

      at Object.<anonymous> (src/configcat/useFeatureFlag.test.ts:35:18)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
      at node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:7
      at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:14:12)

Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        0.676 s, estimated 3 s
Ran all test suites matching /useFeatureFlag/i.

One interesting thing that I discovered is that if I call setIsReady twice, everything seems to work. I tried to experiment with promises, async functions, fake timers etc, but no permutation worked so far. What do I miss?