Skip to content

Commit eb7a1c9

Browse files
authored
Fix useFeatureFlag to re-evaluate on default user change (#87)
1 parent 212d072 commit eb7a1c9

3 files changed

Lines changed: 95 additions & 14 deletions

File tree

src/ConfigCatContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { IConfigCatClient } from "@configcat/sdk";
1+
import type { IConfigCatClient, IUser } from "@configcat/sdk";
22
import React from "react";
33

44
export interface ConfigCatContextData {
55
client: IConfigCatClient;
6+
defaultUser?: IUser | null;
67
lastUpdated?: Date;
78
}
89

src/ConfigCatHooks.test.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { PollingMode } from "@configcat/sdk";
2-
import { render, screen } from "@testing-library/react";
1+
import { IUser, PollingMode } from "@configcat/sdk";
2+
import { fireEvent, render, screen } from "@testing-library/react";
33
import React, { useEffect, useState } from "react";
44
import { vi } from "vitest";
55
import { useConfigCatClient, useFeatureFlag } from "./ConfigCatHooks";
@@ -92,6 +92,43 @@ it("useFeatureFlag Manual poll with forceRefresh should work", async () => {
9292
await screen.findByText("Feature flag value: Cat", void 0, { timeout: 2000 });
9393
});
9494

95+
it("useFeatureFlag should pick up changed default user", async () => {
96+
const defaultUser: IUser = { identifier: "0", email: "test@configcat.com" };
97+
98+
const TestComponent = () => {
99+
const client = useConfigCatClient();
100+
const [user, setUser] = useState<IUser | null>(defaultUser);
101+
useEffect(() => user ? client.setDefaultUser(user) : client.clearDefaultUser(), [client, user]);
102+
const { value: featureFlag } = useFeatureFlag("stringContainsDogDefaultCat", "NOT_CAT");
103+
return (
104+
<>
105+
<div>Feature flag value: {featureFlag}</div>
106+
<button onClick={() => setUser(defaultUser)}>Set default user</button>
107+
<button onClick={() => setUser(null)}>Clear default user</button>
108+
</>
109+
);
110+
};
111+
112+
await render(<ConfigCatProvider sdkKey={sdkKey} options={{ defaultUser }}><TestComponent /></ConfigCatProvider>);
113+
const flagValueDiv = await screen.findByText("Feature flag value: Dog", void 0, { timeout: 2000 });
114+
115+
let button = screen.getByText("Clear default user");
116+
fireEvent.click(button);
117+
118+
// Allow the component to update.
119+
await new Promise<void>(resolve => setTimeout(() => resolve(), 0));
120+
121+
expect(flagValueDiv.textContent).toBe("Feature flag value: Cat");
122+
123+
button = screen.getByText("Set default user");
124+
fireEvent.click(button);
125+
126+
// Allow the component to update.
127+
await new Promise<void>(resolve => setTimeout(() => resolve(), 0));
128+
129+
expect(flagValueDiv.textContent).toBe("Feature flag value: Dog");
130+
});
131+
95132
it("useFeatureFlag with invalid providerId should fail", () => {
96133
const spy = vi.spyOn(console, "error");
97134
spy.mockImplementation(() => { });

src/ConfigCatProvider.tsx

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type AugmentedConfigCatClient = IConfigCatClient & {
3030

3131
class ConfigCatProvider extends Component<PropsWithChildren<ConfigCatProviderProps>, ConfigCatProviderState, {}> {
3232
private configChangedHandler?: (newConfig: Config) => void;
33+
private originalClearDefaultUser?: () => void;
34+
private originalSetDefaultUser?: (defaultUser: IUser) => void;
3335

3436
constructor(props: ConfigCatProviderProps) {
3537
super(props);
@@ -42,31 +44,72 @@ class ConfigCatProvider extends Component<PropsWithChildren<ConfigCatProviderPro
4244
const providers = client._configCatReactSdkProviders ??= new Set();
4345
providers.add(this);
4446

45-
this.state = { client };
47+
this.state = { client, defaultUser: this.props.options?.defaultUser };
4648
}
4749

4850
componentDidMount(): void {
51+
const { client } = this.state;
52+
53+
// Monkey-patch client's `clearDefaultUser` and `setDefaultUser` methods to detect default user changes.
54+
55+
// eslint-disable-next-line @typescript-eslint/unbound-method
56+
this.originalClearDefaultUser = client.clearDefaultUser;
57+
client.clearDefaultUser = () => {
58+
if (this.originalClearDefaultUser && this.state.client === client) {
59+
this.originalClearDefaultUser.call(client);
60+
this.setState({ defaultUser: void 0 });
61+
}
62+
};
63+
64+
// eslint-disable-next-line @typescript-eslint/unbound-method
65+
this.originalSetDefaultUser = client.setDefaultUser;
66+
client.setDefaultUser = defaultUser => {
67+
if (this.originalSetDefaultUser && this.state.client === client) {
68+
this.originalSetDefaultUser.call(client, defaultUser);
69+
this.setState({ defaultUser });
70+
}
71+
};
72+
73+
// Wire up config data change detection.
74+
4975
this.configChangedHandler = newConfig => this.reactConfigChanged(newConfig);
5076

51-
this.state.client.waitForReady().then(() => {
52-
if (!this.configChangedHandler) {
53-
// If the component was unmounted before client initialization finished, we have nothing left to do.
54-
return;
77+
client.waitForReady().then(() => {
78+
// If the component was unmounted before client initialization finished, we have nothing left to do.
79+
if (this.configChangedHandler && this.state.client === client) {
80+
client.on("configChanged", this.configChangedHandler);
81+
this.clientReady();
5582
}
56-
this.state.client.on("configChanged", this.configChangedHandler);
57-
this.clientReady();
5883
});
5984
}
6085

6186
componentWillUnmount(): void {
87+
const { client } = this.state;
88+
89+
// Stop config data change detection.
90+
6291
if (this.configChangedHandler) {
63-
this.state.client.off("configChanged", this.configChangedHandler);
64-
delete this.configChangedHandler;
92+
client.off("configChanged", this.configChangedHandler);
93+
this.configChangedHandler = void 0;
94+
}
95+
96+
// Restore monkey-patched client methods.
97+
98+
if (this.originalClearDefaultUser) {
99+
client.clearDefaultUser = this.originalClearDefaultUser!;
100+
this.originalClearDefaultUser = void 0;
101+
}
102+
103+
if (this.originalSetDefaultUser) {
104+
client.setDefaultUser = this.originalSetDefaultUser!;
105+
this.originalSetDefaultUser = void 0;
65106
}
66107

67-
const providers = (this.state.client as AugmentedConfigCatClient)._configCatReactSdkProviders;
108+
// Dispose client if no longer in use.
109+
110+
const providers = (client as AugmentedConfigCatClient)._configCatReactSdkProviders;
68111
if (providers?.delete(this) && !providers.size) {
69-
this.state.client.dispose();
112+
client.dispose();
70113
}
71114
}
72115

0 commit comments

Comments
 (0)