diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx index a7a28a7c..adf76875 100644 --- a/packages/react-sdk/src/index.tsx +++ b/packages/react-sdk/src/index.tsx @@ -483,7 +483,13 @@ export function useFlag(key: TKey): TypedFlags[TKey] { client, ); - if (isLoading || !flag) { + // Only fall back to disabled defaults when the client has no value for the + // flag. While the client is initializing it may already hold servable + // values (bootstrapped state, cached flags, or previous-context flags + // during a setContext() refetch) — masking those with `isEnabled: false` + // flashes flag-gated UI. `isLoading` stays exposed for consumers that want + // to render loading states themselves. + if (!flag) { return { key, isLoading, diff --git a/packages/react-sdk/test/usage.test.tsx b/packages/react-sdk/test/usage.test.tsx index f3f69f4a..e4b1ca94 100644 --- a/packages/react-sdk/test/usage.test.tsx +++ b/packages/react-sdk/test/usage.test.tsx @@ -833,6 +833,57 @@ describe("useFlag with ReflagBootstrappedProvider", () => { unmount(); }); + test("never reports a bootstrapped flag as disabled while initializing", async () => { + const bootstrapFlags: BootstrappedFlags = { + context: { + user: { id: "456", name: "test" }, + company: { id: "123", name: "test" }, + other: { test: "test" }, + }, + flags: { + abc: { + key: "abc", + isEnabled: true, + targetingVersion: 1, + config: { + key: "gpt3", + payload: { model: "gpt-something", temperature: 0.5 }, + version: 2, + }, + }, + }, + }; + + // Record every render: waitFor-based assertions skip over transient + // frames, which is exactly where the value used to flash to disabled + // while initialize() passed through the "initializing" state. + const observed: { isEnabled: boolean; isLoading: boolean }[] = []; + + function Probe() { + const { isEnabled, isLoading } = useFlag("abc"); + observed.push({ isEnabled, isLoading }); + return null; + } + + const { unmount } = render( + getBootstrapProvider(bootstrapFlags, { children: }), + ); + + // Wait until initialization has fully completed (isLoading settled back + // to false) so all transient renders have happened. + await waitFor(() => { + expect(ReflagClient.prototype.initialize).toHaveBeenCalled(); + expect(observed.at(-1)?.isLoading).toBe(false); + }); + + expect(observed.length).toBeGreaterThan(0); + // The flag was bootstrapped as enabled — no render may report it + // disabled, including renders where the client is initializing. + expect(observed.every((o) => o.isEnabled)).toBe(true); + + unmount(); + }); + // Removed test "returns loading state when no flags are bootstrapped" // because ReflagBootstrappedProvider requires flags to be provided });