Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ConfigCatContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
41 changes: 39 additions & 2 deletions src/ConfigCatHooks.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -92,6 +92,43 @@
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<IUser | null>(defaultUser);
useEffect(() => user ? client.setDefaultUser(user) : client.clearDefaultUser(), [client, user]);
const { value: featureFlag } = useFeatureFlag("stringContainsDogDefaultCat", "NOT_CAT");
return (
<>
<div>Feature flag value: {featureFlag}</div>
<button onClick={() => setUser(defaultUser)}>Set default user</button>
<button onClick={() => setUser(null)}>Clear default user</button>
</>
);
};

await render(<ConfigCatProvider sdkKey={sdkKey} options={{ defaultUser }}><TestComponent /></ConfigCatProvider>);

Check failure on line 112 in src/ConfigCatHooks.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=configcat_react-sdk&issues=AZ5pP8-L4mEyNuHWbeKX&open=AZ5pP8-L4mEyNuHWbeKX&pullRequest=87
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<void>(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<void>(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(() => { });
Expand Down
65 changes: 54 additions & 11 deletions src/ConfigCatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type AugmentedConfigCatClient = IConfigCatClient & {

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

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

this.state = { client };
this.state = { client, defaultUser: this.props.options?.defaultUser };
}

componentDidMount(): void {
const { client } = this.state;

// Monkey-patch client's `clearDefaultUser` and `setDefaultUser` methods to detect default user changes.

// eslint-disable-next-line @typescript-eslint/unbound-method
this.originalClearDefaultUser = client.clearDefaultUser;
client.clearDefaultUser = () => {
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();
}
}

Expand Down
Loading