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?