diff --git a/README.md b/README.md
index 30aec4c..987df64 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Below is a simple example that describes the instantiation and most basic usage
import React from 'react';
// Import SDK functions
-import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react';
+import { SplitFactoryProvider, useTreatment } from '@splitsoftware/splitio-react';
// Define your config object
const CONFIG = {
@@ -29,18 +29,18 @@ const CONFIG = {
};
function MyComponent() {
- // Evaluate feature flags with useSplitTreatments hook
- const { treatments: { FEATURE_FLAG_NAME }, isReady } = useSplitTreatments({ names: ['FEATURE_FLAG_NAME'] });
+ // Evaluate a feature flag with useTreatment hook
+ const { treatment, isReady } = useTreatment({ name: 'FEATURE_FLAG_NAME' });
// Check SDK readiness using isReady prop
if (!isReady) return
Loading SDK ...
;
- if (FEATURE_FLAG_NAME.treatment === 'on') {
- // return JSX for on treatment
- } else if (FEATURE_FLAG_NAME.treatment === 'off') {
- // return JSX for off treatment
+ if (treatment === 'on') {
+ // return JSX for 'on' treatment
+ } else if (treatment === 'off') {
+ // return JSX for 'off' treatment
} else {
- // return JSX for control treatment
+ // return JSX for 'control' treatment
};
}
diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx
index fd146ae..e94984d 100644
--- a/src/SplitClient.tsx
+++ b/src/SplitClient.tsx
@@ -9,8 +9,6 @@ import { useSplitClient } from './useSplitClient';
*
* The underlying SDK client can be changed during the component lifecycle
* if the component is updated with a different splitKey prop.
- *
- * @deprecated `SplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook.
*/
export function SplitClient(props: ISplitClientProps) {
const { children } = props;
diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx
index 7f52776..e895f72 100644
--- a/src/SplitTreatments.tsx
+++ b/src/SplitTreatments.tsx
@@ -9,7 +9,7 @@ import { useSplitTreatments } from './useSplitTreatments';
* call the 'client.getTreatmentsWithConfig()' method if the `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method
* if the `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function.
*
- * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
+ * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function SplitTreatments(props: ISplitTreatmentsProps) {
const { children } = props;
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index a455af6..d63b749 100644
--- a/src/__tests__/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -12,6 +12,10 @@ import {
useSplitClient as exportedUseSplitClient,
useSplitTreatments as exportedUseSplitTreatments,
useSplitManager as exportedUseSplitManager,
+ useTreatment as exportedUseTreatment,
+ useTreatmentWithConfig as exportedUseTreatmentWithConfig,
+ useTreatments as exportedUseTreatments,
+ useTreatmentsWithConfig as exportedUseTreatmentsWithConfig,
// Checks that types are exported. Otherwise, the test would fail with a TS error.
GetTreatmentsOptions,
ISplitClientChildProps,
@@ -39,6 +43,10 @@ import { useTrack } from '../useTrack';
import { useSplitClient } from '../useSplitClient';
import { useSplitTreatments } from '../useSplitTreatments';
import { useSplitManager } from '../useSplitManager';
+import { useTreatment } from '../useTreatment';
+import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
+import { useTreatments } from '../useTreatments';
+import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';
describe('index', () => {
@@ -59,6 +67,10 @@ describe('index', () => {
expect(exportedUseSplitClient).toBe(useSplitClient);
expect(exportedUseSplitTreatments).toBe(useSplitTreatments);
expect(exportedUseSplitManager).toBe(useSplitManager);
+ expect(exportedUseTreatment).toBe(useTreatment);
+ expect(exportedUseTreatmentWithConfig).toBe(useTreatmentWithConfig);
+ expect(exportedUseTreatments).toBe(useTreatments);
+ expect(exportedUseTreatmentsWithConfig).toBe(useTreatmentsWithConfig);
});
it('should export SplitContext', () => {
diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts
index b7ea1e2..a6f5ddb 100644
--- a/src/__tests__/testUtils/mockSplitFactory.ts
+++ b/src/__tests__/testUtils/mockSplitFactory.ts
@@ -1,6 +1,7 @@
import { EventEmitter } from 'events';
import jsSdkPackageJson from '@splitsoftware/splitio/package.json';
import reactSdkPackageJson from '../../../package.json';
+import { CONTROL, CONTROL_WITH_CONFIG } from '../../constants';
export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`;
export const reactSdkVersion = `react-${reactSdkPackageJson.version}`;
@@ -65,6 +66,24 @@ export function mockSdk() {
const track: jest.Mock = jest.fn(() => {
return true;
});
+ const getTreatment: jest.Mock = jest.fn((featureFlagName: string) => {
+ return typeof featureFlagName === 'string' ? 'on' : CONTROL;
+ });
+ const getTreatments: jest.Mock = jest.fn((featureFlagNames: string[]) => {
+ return featureFlagNames.reduce((result: SplitIO.Treatments, featureName: string) => {
+ result[featureName] = 'on';
+ return result;
+ }, {});
+ });
+ const getTreatmentsByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => {
+ return flagSets.reduce((result: SplitIO.Treatments, flagSet: string) => {
+ result[flagSet + '_feature_flag'] = 'on';
+ return result;
+ }, {});
+ });
+ const getTreatmentWithConfig: jest.Mock = jest.fn((featureFlagName: string) => {
+ return typeof featureFlagName === 'string' ? { treatment: 'on', config: null } : CONTROL_WITH_CONFIG;
+ });
const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => {
return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => {
result[featureName] = { treatment: 'on', config: null };
@@ -113,6 +132,10 @@ export function mockSdk() {
});
return Object.assign(Object.create(__emitter__), {
+ getTreatment,
+ getTreatments,
+ getTreatmentsByFlagSets,
+ getTreatmentWithConfig,
getTreatmentsWithConfig,
getTreatmentsWithConfigByFlagSets,
track,
diff --git a/src/__tests__/useTreatment.test.tsx b/src/__tests__/useTreatment.test.tsx
new file mode 100644
index 0000000..106131e
--- /dev/null
+++ b/src/__tests__/useTreatment.test.tsx
@@ -0,0 +1,174 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser } from './testUtils/sdkConfigs';
+import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatment } from '../useTreatment';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentResult } from '../types';
+
+describe('useTreatment', () => {
+
+ const featureFlagName = 'split1';
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatment: SplitIO.Treatment;
+
+ render(
+
+ {React.createElement(() => {
+ treatment = useTreatment({ name: featureFlagName, attributes, properties }).treatment;
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatment` method
+ expect(client.getTreatment).not.toBeCalled();
+ expect(treatment!).toEqual(CONTROL);
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatment).toBeCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatment).toHaveReturnedWith(treatment!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatment = useTreatment({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatment` method
+ expect(client.getTreatment).not.toBeCalled();
+ expect(treatment).toEqual(CONTROL);
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatment).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatment).toHaveLastReturnedWith(treatment);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatment).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatment({ name: featureFlagName, attributes }).treatment;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ test('useTreatment must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatment = 0, countUseTreatmentUser2 = 0, countUseTreatmentUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatment).toEqual('on')
+ } else {
+ expect(treatment).toEqual('control')
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatment(context);
+ countUseTreatment++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatment evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatment).toEqual(4);
+ expect(mainClient.getTreatment).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatment).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);
+
+ // If useTreatment evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatment).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatment).toHaveBeenLastCalledWith('split_test', undefined, undefined);
+ });
+
+});
diff --git a/src/__tests__/useTreatmentWithConfig.test.tsx b/src/__tests__/useTreatmentWithConfig.test.tsx
new file mode 100644
index 0000000..a296126
--- /dev/null
+++ b/src/__tests__/useTreatmentWithConfig.test.tsx
@@ -0,0 +1,174 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser } from './testUtils/sdkConfigs';
+import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentWithConfigResult } from '../types';
+
+describe('useTreatmentWithConfig', () => {
+
+ const featureFlagName = 'split1';
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatment: SplitIO.TreatmentWithConfig;
+
+ render(
+
+ {React.createElement(() => {
+ treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties }).treatment;
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentWithConfig` method
+ expect(client.getTreatmentWithConfig).not.toBeCalled();
+ expect(treatment!).toEqual(CONTROL_WITH_CONFIG);
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatmentWithConfig).toBeCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatmentWithConfig).toHaveReturnedWith(treatment!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatmentWithConfig` method
+ expect(client.getTreatmentWithConfig).not.toBeCalled();
+ expect(treatment).toEqual(CONTROL_WITH_CONFIG);
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatmentWithConfig).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatmentWithConfig).toHaveLastReturnedWith(treatment);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatmentWithConfig).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatmentWithConfig({ name: featureFlagName, attributes }).treatment;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ test('useTreatmentWithConfig must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatmentWithConfig = 0, countUseTreatmentWithConfigUser2 = 0, countUseTreatmentWithConfigUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentWithConfigResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatment).toEqual({ treatment: 'on', config: null })
+ } else {
+ expect(treatment).toEqual({ treatment: 'control', config: null })
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatment(context);
+ countUseTreatmentWithConfig++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentWithConfigUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentWithConfigUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatmentWithConfig evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatmentWithConfig).toEqual(4);
+ expect(mainClient.getTreatmentWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);
+
+ // If useTreatmentWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentWithConfigUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentWithConfigUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatmentWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', undefined, undefined);
+ });
+
+});
diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx
new file mode 100644
index 0000000..9b5bb57
--- /dev/null
+++ b/src/__tests__/useTreatments.test.tsx
@@ -0,0 +1,229 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser } from './testUtils/sdkConfigs';
+import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatments } from '../useTreatments';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentsResult } from '../types';
+
+describe('useTreatments', () => {
+
+ const featureFlagNames = ['split1'];
+ const flagSets = ['set1'];
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatments evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatments: SplitIO.Treatments;
+ let treatmentsByFlagSets: SplitIO.Treatments;
+
+ render(
+
+ {React.createElement(() => {
+ treatments = useTreatments({ names: featureFlagNames, attributes, properties }).treatments;
+ treatmentsByFlagSets = useTreatments({ flagSets, attributes, properties }).treatments;
+
+ // @ts-expect-error Options object must provide either names or flagSets
+ expect(useTreatments({}).treatments).toEqual({});
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatments` method
+ expect(client.getTreatments).not.toBeCalled();
+ expect(treatments!).toEqual({ split1: CONTROL });
+
+ // returns empty treatments object if not operational, without calling `getTreatmentsByFlagSets` method
+ expect(client.getTreatmentsByFlagSets).not.toBeCalled();
+ expect(treatmentsByFlagSets!).toEqual({});
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatments).toBeCalledWith(featureFlagNames, attributes, { properties });
+ expect(client.getTreatments).toHaveReturnedWith(treatments!);
+
+ expect(client.getTreatmentsByFlagSets).toBeCalledWith(flagSets, attributes, { properties });
+ expect(client.getTreatmentsByFlagSets).toHaveReturnedWith(treatmentsByFlagSets!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatments = useTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatments` method
+ expect(client.getTreatments).not.toBeCalled();
+ expect(treatments).toEqual({ split1: CONTROL });
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatments).toHaveBeenLastCalledWith(featureFlagNames, attributes, { properties });
+ expect(client.getTreatments).toHaveLastReturnedWith(treatments);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatments).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatments({ names: featureFlagNames, attributes }).treatments;
+ useTreatments({ flagSets: featureFlagNames }).treatments;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ /**
+ * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready.
+ */
+ test('Input validation: invalid names are sanitized.', () => {
+ render(
+
+ {
+ React.createElement(() => {
+ // @ts-expect-error Test error handling
+ let treatments = useTreatments('split1').treatments;
+ expect(treatments).toEqual({});
+ // @ts-expect-error Test error handling
+ treatments = useTreatments({ names: [true, ' flag_1 ', ' '] }).treatments;
+ expect(treatments).toEqual({ flag_1: CONTROL });
+
+ return null;
+ })
+ }
+
+ );
+ });
+
+ test('useTreatments must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatments = 0, countUseTreatmentsUser2 = 0, countUseTreatmentsUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatments).toEqual({
+ split_test: 'on'
+ })
+ } else {
+ expect(treatments).toEqual({
+ split_test: 'control'
+ })
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatments(context);
+ countUseTreatments++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatments(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentsUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatments(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentsUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatments evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatments).toEqual(4);
+ expect(mainClient.getTreatments).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatments).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined);
+
+ // If useTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentsUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentsUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatments).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatments).toHaveBeenLastCalledWith(['split_test'], undefined, undefined);
+ });
+
+ test('ignores flagSets if both names and flagSets params are provided.', () => {
+ render(
+
+ {
+ React.createElement(() => {
+ // @ts-expect-error names and flagSets are mutually exclusive
+ const treatments = useTreatments({ names: featureFlagNames, flagSets, attributes }).treatments;
+ expect(treatments).toEqual({ split1: CONTROL });
+ return null;
+ })
+ }
+
+ );
+ });
+
+});
diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useTreatmentsWithConfig.test.tsx
similarity index 77%
rename from src/__tests__/useSplitTreatments.test.tsx
rename to src/__tests__/useTreatmentsWithConfig.test.tsx
index bf32348..84b9981 100644
--- a/src/__tests__/useSplitTreatments.test.tsx
+++ b/src/__tests__/useTreatmentsWithConfig.test.tsx
@@ -12,11 +12,11 @@ import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
/** Test target */
import { SplitFactoryProvider } from '../SplitFactoryProvider';
-import { useSplitTreatments } from '../useSplitTreatments';
+import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';
import { SplitContext } from '../SplitContext';
-import { ISplitTreatmentsChildProps } from '../types';
+import { IUseTreatmentsWithConfigResult } from '../types';
-describe('useSplitTreatments', () => {
+describe('useTreatmentsWithConfig', () => {
const featureFlagNames = ['split1'];
const flagSets = ['set1'];
@@ -32,11 +32,11 @@ describe('useSplitTreatments', () => {
render(
{React.createElement(() => {
- treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties }).treatments;
- treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes, properties }).treatments;
+ treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties }).treatments;
+ treatmentsByFlagSets = useTreatmentsWithConfig({ flagSets, attributes, properties }).treatments;
// @ts-expect-error Options object must provide either names or flagSets
- expect(useSplitTreatments({}).treatments).toEqual({});
+ expect(useTreatmentsWithConfig({}).treatments).toEqual({});
return null;
})}
@@ -68,7 +68,7 @@ describe('useSplitTreatments', () => {
render(
{React.createElement(() => {
- const treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
+ const treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
renderTimes++;
switch (renderTimes) {
@@ -102,8 +102,8 @@ describe('useSplitTreatments', () => {
expect(() => {
render(
React.createElement(() => {
- useSplitTreatments({ names: featureFlagNames, attributes }).treatments;
- useSplitTreatments({ flagSets: featureFlagNames }).treatments;
+ useTreatmentsWithConfig({ names: featureFlagNames, attributes }).treatments;
+ useTreatmentsWithConfig({ flagSets: featureFlagNames }).treatments;
return null;
})
);
@@ -119,10 +119,10 @@ describe('useSplitTreatments', () => {
{
React.createElement(() => {
// @ts-expect-error Test error handling
- let treatments = useSplitTreatments('split1').treatments;
+ let treatments = useTreatmentsWithConfig('split1').treatments;
expect(treatments).toEqual({});
// @ts-expect-error Test error handling
- treatments = useSplitTreatments({ names: [true, ' flag_1 ', ' '] }).treatments;
+ treatments = useTreatmentsWithConfig({ names: [true, ' flag_1 ', ' '] }).treatments;
expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG });
return null;
@@ -132,16 +132,16 @@ describe('useSplitTreatments', () => {
);
});
- test('useSplitTreatments must update on SDK events', async () => {
+ test('useTreatmentsWithConfig must update on SDK events', async () => {
const outerFactory = SplitFactory(sdkBrowser);
const mainClient = outerFactory.client() as any;
const user2Client = outerFactory.client('user_2') as any;
- let countSplitContext = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithoutUpdate = 0;
+ let countSplitContext = 0, countUseTreatmentsWithConfig = 0, countUseTreatmentsWithConfigUser2 = 0, countUseTreatmentsWithConfigUser2WithoutUpdate = 0;
const lastUpdateSetUser2 = new Set();
const lastUpdateSetUser2WithUpdate = new Set();
- function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) {
+ function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsWithConfigResult) {
if (isReady || isReadyFromCache) {
expect(treatments).toEqual({
split_test: {
@@ -166,26 +166,26 @@ describe('useSplitTreatments', () => {
{() => countSplitContext++}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], attributes: { att1: 'att1' } });
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
validateTreatments(context);
- countUseSplitTreatments++;
+ countUseTreatmentsWithConfig++;
return null;
})}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2' });
expect(context.client).toBe(user2Client);
validateTreatments(context);
lastUpdateSetUser2.add(context.lastUpdate);
- countUseSplitTreatmentsUser2++;
+ countUseTreatmentsWithConfigUser2++;
return null;
})}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
expect(context.client).toBe(user2Client);
validateTreatments(context);
lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
- countUseSplitTreatmentsUser2WithoutUpdate++;
+ countUseTreatmentsWithConfigUser2WithoutUpdate++;
return null;
})}
>
@@ -202,16 +202,16 @@ describe('useSplitTreatments', () => {
// SplitFactoryProvider renders once
expect(countSplitContext).toEqual(1);
- // If useSplitTreatments evaluates with the main client and have default update options, it re-renders for each main client event.
- expect(countUseSplitTreatments).toEqual(4);
+ // If useTreatmentsWithConfig evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatmentsWithConfig).toEqual(4);
expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined);
- // If useSplitTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client.
- expect(countUseSplitTreatmentsUser2).toEqual(4);
+ // If useTreatmentsWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentsWithConfigUser2).toEqual(4);
expect(lastUpdateSetUser2.size).toEqual(4);
// If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
- expect(countUseSplitTreatmentsUser2WithoutUpdate).toEqual(3);
+ expect(countUseTreatmentsWithConfigUser2WithoutUpdate).toEqual(3);
expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined, undefined);
@@ -223,7 +223,7 @@ describe('useSplitTreatments', () => {
{
React.createElement(() => {
// @ts-expect-error names and flagSets are mutually exclusive
- const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments;
+ const treatments = useTreatmentsWithConfig({ names: featureFlagNames, flagSets, attributes }).treatments;
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG });
return null;
})
diff --git a/src/withSplitTreatments.tsx b/src/withSplitTreatments.tsx
index 8119292..54bf4fa 100644
--- a/src/withSplitTreatments.tsx
+++ b/src/withSplitTreatments.tsx
@@ -10,7 +10,7 @@ import { SplitTreatments } from './SplitTreatments';
* @param names - list of feature flag names
* @param attributes - An object of type Attributes used to evaluate the feature flags.
*
- * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
+ * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes) {