From 9573f06c36cf8fa553aa26809908188f3664bc1b Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 27 May 2026 13:22:08 +0200 Subject: [PATCH] Fix useFeatureFlag to re-evaluate on default user change --- src/ConfigCatContext.tsx | 3 +- src/ConfigCatHooks.test.tsx | 41 +++++++++++++++++++++-- src/ConfigCatProvider.tsx | 65 ++++++++++++++++++++++++++++++------- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/ConfigCatContext.tsx b/src/ConfigCatContext.tsx index 55100b2..3d0c483 100644 --- a/src/ConfigCatContext.tsx +++ b/src/ConfigCatContext.tsx @@ -1,8 +1,9 @@ -import type { IConfigCatClient } from "@configcat/sdk"; +import type { IConfigCatClient, IUser } from "@configcat/sdk"; import React from "react"; export interface ConfigCatContextData { client: IConfigCatClient; + defaultUser?: IUser | null; lastUpdated?: Date; } diff --git a/src/ConfigCatHooks.test.tsx b/src/ConfigCatHooks.test.tsx index d543e71..06080ba 100644 --- a/src/ConfigCatHooks.test.tsx +++ b/src/ConfigCatHooks.test.tsx @@ -1,5 +1,5 @@ -import { PollingMode } from "@configcat/sdk"; -import { render, screen } from "@testing-library/react"; +import { IUser, PollingMode } from "@configcat/sdk"; +import { fireEvent, render, screen } from "@testing-library/react"; import React, { useEffect, useState } from "react"; import { vi } from "vitest"; import { useConfigCatClient, useFeatureFlag } from "./ConfigCatHooks"; @@ -92,6 +92,43 @@ it("useFeatureFlag Manual poll with forceRefresh should work", async () => { await screen.findByText("Feature flag value: Cat", void 0, { timeout: 2000 }); }); +it("useFeatureFlag should pick up changed default user", async () => { + const defaultUser: IUser = { identifier: "0", email: "test@configcat.com" }; + + const TestComponent = () => { + const client = useConfigCatClient(); + const [user, setUser] = useState(defaultUser); + useEffect(() => user ? client.setDefaultUser(user) : client.clearDefaultUser(), [client, user]); + const { value: featureFlag } = useFeatureFlag("stringContainsDogDefaultCat", "NOT_CAT"); + return ( + <> +
Feature flag value: {featureFlag}
+ + + + ); + }; + + await render(); + const flagValueDiv = await screen.findByText("Feature flag value: Dog", void 0, { timeout: 2000 }); + + let button = screen.getByText("Clear default user"); + fireEvent.click(button); + + // Allow the component to update. + await new Promise(resolve => setTimeout(() => resolve(), 0)); + + expect(flagValueDiv.textContent).toBe("Feature flag value: Cat"); + + button = screen.getByText("Set default user"); + fireEvent.click(button); + + // Allow the component to update. + await new Promise(resolve => setTimeout(() => resolve(), 0)); + + expect(flagValueDiv.textContent).toBe("Feature flag value: Dog"); +}); + it("useFeatureFlag with invalid providerId should fail", () => { const spy = vi.spyOn(console, "error"); spy.mockImplementation(() => { }); diff --git a/src/ConfigCatProvider.tsx b/src/ConfigCatProvider.tsx index 2866cee..b0ec32f 100644 --- a/src/ConfigCatProvider.tsx +++ b/src/ConfigCatProvider.tsx @@ -30,6 +30,8 @@ type AugmentedConfigCatClient = IConfigCatClient & { class ConfigCatProvider extends Component, ConfigCatProviderState, {}> { private configChangedHandler?: (newConfig: Config) => void; + private originalClearDefaultUser?: () => void; + private originalSetDefaultUser?: (defaultUser: IUser) => void; constructor(props: ConfigCatProviderProps) { super(props); @@ -42,31 +44,72 @@ class ConfigCatProvider extends Component { + if (this.originalClearDefaultUser && this.state.client === client) { + this.originalClearDefaultUser.call(client); + this.setState({ defaultUser: void 0 }); + } + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method + this.originalSetDefaultUser = client.setDefaultUser; + client.setDefaultUser = defaultUser => { + if (this.originalSetDefaultUser && this.state.client === client) { + this.originalSetDefaultUser.call(client, defaultUser); + this.setState({ defaultUser }); + } + }; + + // Wire up config data change detection. + this.configChangedHandler = newConfig => this.reactConfigChanged(newConfig); - this.state.client.waitForReady().then(() => { - if (!this.configChangedHandler) { - // If the component was unmounted before client initialization finished, we have nothing left to do. - return; + client.waitForReady().then(() => { + // If the component was unmounted before client initialization finished, we have nothing left to do. + if (this.configChangedHandler && this.state.client === client) { + client.on("configChanged", this.configChangedHandler); + this.clientReady(); } - this.state.client.on("configChanged", this.configChangedHandler); - this.clientReady(); }); } componentWillUnmount(): void { + const { client } = this.state; + + // Stop config data change detection. + if (this.configChangedHandler) { - this.state.client.off("configChanged", this.configChangedHandler); - delete this.configChangedHandler; + client.off("configChanged", this.configChangedHandler); + this.configChangedHandler = void 0; + } + + // Restore monkey-patched client methods. + + if (this.originalClearDefaultUser) { + client.clearDefaultUser = this.originalClearDefaultUser!; + this.originalClearDefaultUser = void 0; + } + + if (this.originalSetDefaultUser) { + client.setDefaultUser = this.originalSetDefaultUser!; + this.originalSetDefaultUser = void 0; } - const providers = (this.state.client as AugmentedConfigCatClient)._configCatReactSdkProviders; + // Dispose client if no longer in use. + + const providers = (client as AugmentedConfigCatClient)._configCatReactSdkProviders; if (providers?.delete(this) && !providers.size) { - this.state.client.dispose(); + client.dispose(); } }