Skip to content

Commit

Permalink
add multicontext support
Browse files Browse the repository at this point in the history
  • Loading branch information
andrew-cat committed Sep 19, 2024
1 parent 0b72912 commit 6e5f73e
Show file tree
Hide file tree
Showing 13 changed files with 2,394 additions and 2,275 deletions.
4,468 changes: 2,248 additions & 2,220 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"homepage": "https://configcat.com",
"license": "MIT",
"dependencies": {
"configcat-common": "9.3.0",
"configcat-common": "9.3.1",
"tslib": "^2.4.1"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@types/node": "^16.11.49",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"configcat-react": "^4.1.0",
"configcat-react": "^4.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
Expand Down
7 changes: 4 additions & 3 deletions sandbox/src/stories/Hoc.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { ConfigCatProvider, LogLevel, withConfigCatClient, WithConfigCatClientProps } from 'configcat-react';
import { User } from 'configcat-common';


class HocComponent extends React.Component<
export class HocComponent extends React.Component<
{ featureFlagKey: string } & WithConfigCatClientProps,
{ isEnabled: boolean, loading: boolean }
> {
constructor(props: { featureFlagKey: string } & WithConfigCatClientProps) {
constructor(props: { featureFlagKey: string, user?: User } & WithConfigCatClientProps) {
super(props);

this.state = { isEnabled: false, loading: true };
Expand All @@ -25,7 +26,7 @@ class HocComponent extends React.Component<

evaluateFeatureFlag() {
this.props
.getValue(this.props.featureFlagKey, false)
.getValue(this.props.featureFlagKey, false, this.props.user)
.then((v: boolean) => this.setState({ isEnabled: v, loading: false }));
}

Expand Down
19 changes: 19 additions & 0 deletions sandbox/src/stories/MultipleConfigCatConfigs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MultipleConfigCatConfigs } from './MultipleConfigCatConfigs';

export default {
title: 'Showcase',
component: MultipleConfigCatConfigs,
parameters: {
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
layout: 'padded',
},
} as ComponentMeta<typeof MultipleConfigCatConfigs>;

const Template: ComponentStory<typeof MultipleConfigCatConfigs> = (args) => <MultipleConfigCatConfigs />;

export const MultipleConfigcatConfigs = Template.bind({});



56 changes: 56 additions & 0 deletions sandbox/src/stories/MultipleConfigCatConfigs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { ConfigCatProvider, User, useFeatureFlagByConfigId, withConfigCatClient } from 'configcat-react';
import { HocComponent} from './Hoc';


const CC_CONFIGID = { BACKEND: "BACKEND", SHARED: "SHARED"};

const CC_SDK = {
BACKEND: "TODO - INSERT BACKEND SDK KEY",
SHARED:"TODO - INSERT SHARED SDK KEY"}

const userObject = new User('microFrontendUser1');

export const C1 = (args: { featureFlagKey: string, configId: string }) => {

const { value: isFeatureEnabled, loading } = useFeatureFlagByConfigId(args.configId, args.featureFlagKey, false, userObject);

return (
<div>
{loading ?
(<div>Loading...</div>) :
(<div>
{args.featureFlagKey} evaluated to {isFeatureEnabled ? 'True' : 'False'}
</div>)}
</div>
);
};

const ConfigCatHocComponent = withConfigCatClient(HocComponent, CC_CONFIGID.BACKEND)

export const MultipleConfigCatConfigs = () => {

return (
<article>
<h1>Embeded provider test</h1>

<div>
<ConfigCatProvider sdkKey={CC_SDK.SHARED} options={{pollIntervalSeconds:10}} configId={CC_CONFIGID.SHARED}>

<C1 featureFlagKey={"sharedfeature1"} configId={CC_CONFIGID.SHARED}></C1>

<ConfigCatProvider sdkKey={CC_SDK.BACKEND} options={{pollIntervalSeconds:10}} configId={CC_CONFIGID.BACKEND}>
<C1 featureFlagKey={"isDebugModeOn"} configId={CC_CONFIGID.BACKEND}></C1>
<C1 featureFlagKey={"sharedfeature1"} configId={CC_CONFIGID.SHARED}></C1>


{/* Higher-Order Components sample */}
<ConfigCatHocComponent featureFlagKey={"isDebugModeOn"} user={userObject}/>
</ConfigCatProvider>

</ConfigCatProvider>
</div>

</article>
);
};
19 changes: 19 additions & 0 deletions src/ConfigCatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,23 @@ const ConfigCatContext = React.createContext<ConfigCatContextData | undefined>(

ConfigCatContext.displayName = "ConfigCatContext";

const ConfigCatContextRegistry = new Map<string, React.Context<ConfigCatContextData | undefined>>();

export function getOrCreateConfigCatContext(configId: string): React.Context<ConfigCatContextData | undefined> {

let context: React.Context<ConfigCatContextData | undefined> | undefined = ConfigCatContextRegistry.get(configId.toLowerCase());

if (!context) {
context = React.createContext<ConfigCatContextData | undefined>(
void 0
);

context.displayName = "ConfigCatContext_" + configId;

ConfigCatContextRegistry.set(configId.toLowerCase(), context);
}

return context;
}

export default ConfigCatContext;
2 changes: 1 addition & 1 deletion src/ConfigCatHOC.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ it("withConfigCatClient without provider should fail", () => {
return (<TestHocComponentWithConfigCatClient />);
};
expect(() => render(<TestComponent />))
.toThrow("withConfigCatClient must be used within a ConfigCatProvider!");
.toThrow("withConfigCatClient must be used in ConfigCatProvider!");
spy.mockRestore();
});

Expand Down
17 changes: 10 additions & 7 deletions src/ConfigCatHOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import type { IConfigCatClient, SettingTypeOf, SettingValue, User } from "configcat-common";
import React from "react";
import type { ConfigCatContextData } from "./ConfigCatContext";
import { type ConfigCatContextData, getOrCreateConfigCatContext } from "./ConfigCatContext";
import ConfigCatContext from "./ConfigCatContext";
import { createConfigCatProviderError } from "./ConfigCatProvider";

export type GetValueType = <T extends SettingValue>(
key: string,
Expand All @@ -24,15 +25,17 @@ export interface WithConfigCatClientProps {
}

function withConfigCatClient<P>(
WrappedComponent: React.ComponentType<P & WithConfigCatClientProps>
WrappedComponent: React.ComponentType<P & WithConfigCatClientProps>,
configId?: string
): React.ComponentType<Omit<P, keyof WithConfigCatClientProps>> {

const configCatContext = configId ? getOrCreateConfigCatContext(configId) : ConfigCatContext;

return (props: P) => (
<ConfigCatContext.Consumer>
<configCatContext.Consumer>
{(context: ConfigCatContextData | undefined) => {
if (!context) {
throw new Error(
"withConfigCatClient must be used within a ConfigCatProvider!"
);
throw createConfigCatProviderError("withConfigCatClient", configId);
}
return (
<WrappedComponent
Expand All @@ -43,7 +46,7 @@ function withConfigCatClient<P>(
/>
);
}}
</ConfigCatContext.Consumer>
</configCatContext.Consumer>
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/ConfigCatHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ it("useConfigCatClient without provider should fail", () => {
return (< div />);
};
expect(() => render(<TestComponent />))
.toThrow("useConfigCatClient hook must be used in ConfigCatProvider!");
.toThrow("useConfigCatClient must be used in ConfigCatProvider!");
spy.mockRestore();
});

Expand All @@ -39,7 +39,7 @@ it("useFeatureFlag without provider should fail", () => {
return (< div />);
};
expect(() => render(<TestComponent />))
.toThrow("useFeatureFlag hook must be used in ConfigCatProvider!");
.toThrow("useFeatureFlag must be used in ConfigCatProvider!");

spy.mockRestore();
});
Expand Down
14 changes: 8 additions & 6 deletions src/ConfigCatHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

import type { IConfigCatClient, SettingTypeOf, SettingValue, User } from "configcat-common";
import { useContext, useEffect, useState } from "react";
import ConfigCatContext from "./ConfigCatContext";
import ConfigCatContext, { getOrCreateConfigCatContext } from "./ConfigCatContext";
import { createConfigCatProviderError } from "./ConfigCatProvider";

function useFeatureFlag<T extends SettingValue>(key: string, defaultValue: T, user?: User): {
function useFeatureFlag<T extends SettingValue>(key: string, defaultValue: T, user?: User, configId?: string): {
value: SettingTypeOf<T>;
loading: boolean;
} {
const configCatContext = useContext(ConfigCatContext);

if (configCatContext === void 0) throw Error("useFeatureFlag hook must be used in ConfigCatProvider!");
if (!configCatContext) throw createConfigCatProviderError("useFeatureFlag", configId);

const [featureFlagValue, setFeatureFlag] = useState(defaultValue as SettingTypeOf<T>);
const [loading, setLoading] = useState(true);
Expand All @@ -23,10 +24,11 @@ function useFeatureFlag<T extends SettingValue>(key: string, defaultValue: T, us
return { value: featureFlagValue, loading };
}

function useConfigCatClient(): IConfigCatClient {
const configCatContext = useContext(ConfigCatContext);
function useConfigCatClient(configId?: string): IConfigCatClient {

const configCatContext = useContext(configId ? getOrCreateConfigCatContext(configId) : ConfigCatContext);

if (!configCatContext) throw Error("useConfigCatClient hook must be used in ConfigCatProvider!");
if (!configCatContext) throw createConfigCatProviderError("useConfigCatClient", configId);

return configCatContext.client;
}
Expand Down
55 changes: 23 additions & 32 deletions src/ConfigCatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PollingMode, getClient } from "configcat-common";
import type { PropsWithChildren } from "react";
import React, { Component } from "react";
import { LocalStorageCache } from "./Cache";
import ConfigCatContext from "./ConfigCatContext";
import ConfigCatContext, { type ConfigCatContextData, getOrCreateConfigCatContext } from "./ConfigCatContext";
import { HttpConfigFetcher } from "./ConfigFetcher";
import CONFIGCAT_SDK_VERSION from "./Version";
import type { IReactAutoPollOptions, IReactLazyLoadingOptions, IReactManualPollOptions } from ".";
Expand All @@ -14,6 +14,7 @@ type ConfigCatProviderProps = {
sdkKey: string;
pollingMode?: PollingMode;
options?: IReactAutoPollOptions | IReactLazyLoadingOptions | IReactManualPollOptions;
configId?: string;
};

type ConfigCatProviderState = {
Expand Down Expand Up @@ -84,10 +85,13 @@ class ConfigCatProvider extends Component<PropsWithChildren<ConfigCatProviderPro
}

render(): JSX.Element {

const context: React.Context<ConfigCatContextData | undefined> = this.props.configId ? getOrCreateConfigCatContext(this.props.configId) : ConfigCatContext;

return (
<ConfigCatContext.Provider value={this.state}>
<context.Provider value={this.state}>
{this.props.children}
</ConfigCatContext.Provider>
</context.Provider>
);
}
}
Expand All @@ -104,97 +108,84 @@ function serverContextNotSupported(): Error {
}

class ConfigCatClientStub implements IConfigCatClient {
readonly isOffline = true;

getValueAsync<T extends SettingValue>(_key: string, _defaultValue: T, _user?: User | undefined): Promise<SettingTypeOf<T>> {
getValueAsync<T extends SettingValue>(_key: string, _defaultValue: T, _user?: User): Promise<SettingTypeOf<T>> {
throw serverContextNotSupported();
}

getValueDetailsAsync<T extends SettingValue>(_key: string, _defaultValue: T, _user?: User | undefined): Promise<IEvaluationDetails<SettingTypeOf<T>>> {
getValueDetailsAsync<T extends SettingValue>(_key: string, _defaultValue: T, _user?: User): Promise<IEvaluationDetails<SettingTypeOf<T>>> {
throw serverContextNotSupported();
}

getAllKeysAsync(): Promise<string[]> {
throw serverContextNotSupported();
}

getAllValuesAsync(_user?: User | undefined): Promise<SettingKeyValue<SettingValue>[]> {
getAllValuesAsync(_user?: User): Promise<SettingKeyValue<SettingValue>[]> {
throw serverContextNotSupported();
}

getAllValueDetailsAsync(_user?: User | undefined): Promise<IEvaluationDetails<SettingValue>[]> {
getAllValueDetailsAsync(_user?: User): Promise<IEvaluationDetails<SettingValue>[]> {
throw serverContextNotSupported();
}

getKeyAndValueAsync(_variationId: string): Promise<SettingKeyValue<SettingValue> | null> {
throw serverContextNotSupported();
}

forceRefreshAsync(): Promise<RefreshResult> {
throw serverContextNotSupported();
}

waitForReady(): Promise<ClientCacheState> {
throw serverContextNotSupported();
}

snapshot(): IConfigCatClientSnapshot {
throw serverContextNotSupported();
}

setDefaultUser(_defaultUser: User): void {
throw serverContextNotSupported();
}

clearDefaultUser(): void {
throw serverContextNotSupported();
}

isOffline: boolean;
setOnline(): void {
throw serverContextNotSupported();
}

setOffline(): void {
throw serverContextNotSupported();
}

dispose(): void { }

dispose(): void {
throw serverContextNotSupported();
}
addListener<TEventName extends keyof HookEvents>(_eventName: TEventName, _listener: (...args: HookEvents[TEventName]) => void): this {
throw serverContextNotSupported();
}

on<TEventName extends keyof HookEvents>(_eventName: TEventName, _listener: (...args: HookEvents[TEventName]) => void): this {
throw serverContextNotSupported();
}

once<TEventName extends keyof HookEvents>(_eventName: TEventName, _listener: (...args: HookEvents[TEventName]) => void): this {
throw serverContextNotSupported();
}

removeListener<TEventName extends keyof HookEvents>(_eventName: TEventName, _listener: (...args: HookEvents[TEventName]) => void): this {
throw serverContextNotSupported();
}

off<TEventName extends keyof HookEvents>(_eventName: TEventName, _listener: (...args: HookEvents[TEventName]) => void): this {
throw serverContextNotSupported();
}

removeAllListeners(_eventName?: keyof HookEvents | undefined): this {
removeAllListeners(_eventName?: keyof HookEvents): this {
throw serverContextNotSupported();
}

listeners(_eventName: keyof HookEvents): Function[] {
throw serverContextNotSupported();
}

listenerCount(_eventName: keyof HookEvents): number {
throw serverContextNotSupported();
}

eventNames(): (keyof HookEvents)[] {
throw serverContextNotSupported();
}
}

export function createConfigCatProviderError(methodName: string, configId?: string): Error {

const configIdText: string = configId ? ` with configId="${configId}"` : "";

return Error(`${methodName} must be used in ConfigCatProvider${configIdText}!`);
}

export default ConfigCatProvider;
Loading

0 comments on commit 6e5f73e

Please sign in to comment.