From 85dfdf54a21d85f1e1276816b89ef6e7fd4963e9 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Thu, 6 Aug 2026 18:51:53 +0200 Subject: [PATCH] prevent missed opt-in flag updates on mount --- .changeset/calm-pandas-refresh-opt-in.md | 5 ++ packages/react-sdk/src/index.tsx | 1 + packages/react-sdk/test/usage.test.tsx | 61 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .changeset/calm-pandas-refresh-opt-in.md diff --git a/.changeset/calm-pandas-refresh-opt-in.md b/.changeset/calm-pandas-refresh-opt-in.md new file mode 100644 index 000000000..dfa4747dd --- /dev/null +++ b/.changeset/calm-pandas-refresh-opt-in.md @@ -0,0 +1,5 @@ +--- +"@reflag/react-sdk": patch +--- + +Prevent `useOptInFlags()` from missing a flag update that finishes between render and event subscription. diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx index 4039c1c53..8272516d9 100644 --- a/packages/react-sdk/src/index.tsx +++ b/packages/react-sdk/src/index.tsx @@ -753,6 +753,7 @@ export function useOptInFlags( useOnEvent("optInFlagsLoadingUpdated", setIsOptInFlagsLoading, client); useIsomorphicLayoutEffect(() => { + setFlags(getOptInFlags()); setIsOptInFlagsLoading(client.getIsLoadingOptInFlags()); }, [client]); diff --git a/packages/react-sdk/test/usage.test.tsx b/packages/react-sdk/test/usage.test.tsx index 51084dc7e..f1ab0a192 100644 --- a/packages/react-sdk/test/usage.test.tsx +++ b/packages/react-sdk/test/usage.test.tsx @@ -1146,6 +1146,67 @@ describe("opt-in hooks", () => { unmount(); }); + test("does not miss opt-in flags updated between render and subscription", async () => { + const client = new ReflagClient({ + publishableKey: "key-opt-in-mount-race", + offline: true, + }); + const optInFlag = { + key: "optInFlag", + name: "Opt-in flag", + description: null, + isEnabled: false, + userOptedIn: false, + companyOptedIn: false, + isOptedIn: false, + }; + const getOptInFlags = vi + .spyOn(client, "getOptInFlags") + .mockReturnValueOnce([]) + .mockReturnValue([optInFlag]); + + const { result, unmount } = renderHook(() => useOptInFlags(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current).toEqual({ flags: [optInFlag], isLoading: false }); + expect(getOptInFlags).toHaveBeenCalledTimes(2); + + unmount(); + await client.stop(); + }); + + test("returns an empty flags array when regular provider initialization fails", async () => { + const logger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + const initialize = vi + .spyOn(ReflagClient.prototype, "initialize") + .mockRejectedValueOnce(new Error("init failed")); + + const { result, unmount } = renderHook(() => useOptInFlags(), { + wrapper: ({ children }) => getProvider({ children, logger }), + }); + + await waitFor(() => { + expect(logger.error).toHaveBeenCalledWith( + "failed to initialize client", + expect.any(Error), + ); + }); + expect(result.current).toEqual({ flags: [], isLoading: false }); + + unmount(); + initialize.mockRestore(); + }); + test("suspends with a regular provider during the initial flags request", async () => { let resolveResponse!: (response: HttpResponse) => void; const response = new Promise((resolve) => {