Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
/lib
/es
/umd
/types
/coverage
.scannerwork
9 changes: 7 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
1.10.0 (September XX, 2023)
1.10.0 (November XX, 2023)
- Added new `useSplitClient` and `useSplitTreatments` hooks to use instead of `useClient` and `useTreatments` respectively, which are deprecated now.
- The new hooks return the Split context object together with the SDK client and treatments respectively, rather than returning only the client and treatments as the deprecated hooks do. This way it is possible to access status properties, like `isReady`, from the hook's results, without having to use the `useContext` hook or the client `ready` promise.
- They accept an options object as parameter, which support the same arguments than the deprecated hooks, plus new boolean options to control when the hook should re-render: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`.
- `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize calls to the SDK's `getTreatmentsWithConfig` method. This avoids re-evaluating feature flags when the hook is called with the same options and the feature flag definitions have not changed.
- They fixed a bug in the deprecated hooks, which caused them to not re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event).
- Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index. For example, `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react';` (Related to issue https://github.com/splitio/react-client/issues/162).
- Updated type declarations of the library components to not restrict the type of the `children` prop to ReactElement, allowing to pass any valid ReactNode value (Related to issue https://github.com/splitio/react-client/issues/164).
- Updated the `useTreatments` hook to optimize feature flag evaluation. It now uses the `useMemo` hook to memoize calls to the SDK's `getTreatmentsWithConfig` function. This avoids re-evaluating feature flags when the hook is called with the same parameters and the feature flag definitions have not changed.
- Updated the `useTreatments` hook to optimize feature flag evaluations.
- Updated linter and other dependencies for vulnerability fixes.
- Bugfixing - To adhere to the rules of hooks and prevent React warnings, conditional code within hooks was removed. Previously, this code checked for the availability of the hooks API (available in React version 16.8.0 or above) and logged an error message. Now, using hooks with React versions below 16.8.0 will throw an error.
- Bugfixing - Updated `useClient` and `useTreatments` hooks to re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event).
Expand Down
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Below is a simple example that describes the instantiation and most basic usage
import React from 'react';

// Import SDK functions
import { SplitFactory, SplitTreatments } from '@splitsoftware/splitio-react';
import { SplitFactory, useSplitTreatments } from '@splitsoftware/splitio-react';

// Define your config object
const CONFIG = {
Expand All @@ -30,25 +30,27 @@ const CONFIG = {
}
};

function MyReactComponent() {
function MyComponent() {
// Evaluate feature flags with useSplitTreatments hook
const { treatments: { FEATURE_FLAG_NAME }, isReady } = useSplitTreatments({ names: ['FEATURE_FLAG_NAME'] });

// Check SDK readiness using isReady prop
if (!isReady) return <div>Loading SDK ...</div>;

if (FEATURE_FLAG_NAME.treatment === 'on') {
// return JSX for on treatment
} else if (FEATURE_FLAG_NAME.treatment === 'off') {
// return JSX for off treatment
} else {
// return JSX for control treatment
};
}

function MyApp() {
return (
/* Use SplitFactory to instantiate the SDK and makes it available to nested components */
// Use SplitFactory to instantiate the SDK and makes it available to nested components
<SplitFactory config={CONFIG} >
{/* Evaluate feature flags with SplitTreatments component */}
<SplitTreatments names={['FEATURE_FLAG_NAME']} >
{({ treatments: { FEATURE_FLAG_NAME }, isReady }) => {
// Check SDK readiness using isReady prop
if (!isReady)
return <div>Loading SDK ...</div>;
if (FEATURE_FLAG_NAME.treatment === 'on') {
// return JSX for on treatment
} else if (FEATURE_FLAG_NAME.treatment === 'off') {
// return JSX for off treatment
} else {
// return JSX for control treatment
}
}}
</SplitTreatments>
<MyComponent />
</SplitFactory>
);
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-react",
"version": "1.9.0",
"version": "1.9.1-rc.0",
"description": "A React library to easily integrate and use Split JS SDK",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/SplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ describe('SplitTreatments', () => {
}}
</SplitFactory>
);
expect(logSpy).toBeCalledWith('[ERROR] split names must be a non-empty array.');
expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid split name, split name must be a non-empty string.');
expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.');
expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.');

done();
});
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
useManager as exportedUseManager,
useTrack as exportedUseTrack,
useTreatments as exportedUseTreatments,
useSplitClient as exportedUseSplitClient,
useSplitTreatments as exportedUseSplitTreatments,
// Checks that types are exported. Otherwise, the test would fail with a TS error.
ISplitClientChildProps,
ISplitClientProps,
Expand All @@ -35,6 +37,8 @@ import { useClient } from '../useClient';
import { useManager } from '../useManager';
import { useTrack } from '../useTrack';
import { useTreatments } from '../useTreatments';
import { useSplitClient } from '../useSplitClient';
import { useSplitTreatments } from '../useSplitTreatments';

describe('index', () => {

Expand All @@ -55,6 +59,8 @@ describe('index', () => {
expect(exportedUseManager).toBe(useManager);
expect(exportedUseTrack).toBe(useTrack);
expect(exportedUseTreatments).toBe(useTreatments);
expect(exportedUseSplitClient).toBe(useSplitClient);
expect(exportedUseSplitTreatments).toBe(useSplitTreatments);
});

it('should export SplitContext', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/useSplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ describe('useSplitTreatments', () => {
return null;
})
);
expect(logSpy).toBeCalledWith('[ERROR] split names must be a non-empty array.');
expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid split name, split name must be a non-empty string.');
expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.');
expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.');
});

test('useSplitTreatments must update on SDK events', async () => {
Expand Down
32 changes: 18 additions & 14 deletions src/__tests__/withSplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,42 @@ import { sdkBrowser } from './testUtils/sdkConfigs';
import { withSplitFactory } from '../withSplitFactory';
import { withSplitClient } from '../withSplitClient';
import { withSplitTreatments } from '../withSplitTreatments';
import { ISplitTreatmentsChildProps } from '../types';
import { getControlTreatmentsWithConfig } from '../constants';

describe('withSplitTreatments', () => {

it(`passes Split props and outer props to the child.
In this test, the value of "props.treatments" is obteined by the function "getControlTreatmentsWithConfig",
and not "client.getTreatmentsWithConfig" since the client is not ready.`, (done) => {
In this test, the value of "props.treatments" is obtained by the function "getControlTreatmentsWithConfig",
and not "client.getTreatmentsWithConfig" since the client is not ready.`, () => {
const featureFlagNames = ['split1', 'split2'];

const Component = withSplitFactory(sdkBrowser)<{ outerProp1: string, outerProp2: number }>(
({ outerProp1, outerProp2, factory }) => {
const SubComponent = withSplitClient('user1')<{ outerProp1: string, outerProp2: number }>(
withSplitTreatments(featureFlagNames)(
(props: ISplitTreatmentsChildProps & { outerProp1: string, outerProp2: number }) => {
(props) => {
const clientMock = factory!.client('user1');
expect(props.outerProp1).toBe('outerProp1');
expect(props.outerProp2).toBe(2);
expect((clientMock.getTreatmentsWithConfig as jest.Mock).mock.calls.length).toBe(0);
expect(props.treatments).toEqual(getControlTreatmentsWithConfig(featureFlagNames));
expect(props.isReady).toBe(false);
expect(props.isReadyFromCache).toBe(false);
expect(props.hasTimedout).toBe(false);
expect(props.isTimedout).toBe(false);
expect(props.isDestroyed).toBe(false);
expect(props.lastUpdate).toBe(0);
done();

expect(props).toStrictEqual({
factory: factory, client: clientMock,
outerProp1: 'outerProp1', outerProp2: 2,
treatments: getControlTreatmentsWithConfig(featureFlagNames),
isReady: false,
isReadyFromCache: false,
hasTimedout: false,
isTimedout: false,
isDestroyed: false,
lastUpdate: 0
});

return null;
}
)
);
return <SubComponent outerProp1={outerProp1} outerProp2={outerProp2} />;
});

render(<Component outerProp1='outerProp1' outerProp2={2} />);
});

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export { useClient } from './useClient';
export { useTreatments } from './useTreatments';
export { useTrack } from './useTrack';
export { useManager } from './useManager';
export { useSplitClient } from './useSplitClient';
export { useSplitTreatments } from './useSplitTreatments';

// SplitContext
export { SplitContext } from './SplitContext';
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues {
* An object with the treatments with configs for a bulk of feature flags, returned by client.getTreatmentsWithConfig().
* Each existing configuration is a stringified version of the JSON you defined on the Split user interface. For example:
* {
* split1: { treatment: 'on', config: null }
* split2: { treatment: 'off', config: '{"bannerText":"Click here."}' }
* feature1: { treatment: 'on', config: null }
* feature2: { treatment: 'off', config: '{"bannerText":"Click here."}' }
* }
*/
treatments: SplitIO.TreatmentsWithConfig;
Expand Down
2 changes: 2 additions & 0 deletions src/useClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useSplitClient } from './useSplitClient';
*
* @return A Split Client instance, or null if used outside the scope of SplitFactory
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
*
* @deprecated Replace with the new `useSplitClient` hook.
*/
export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null {
return useSplitClient({ splitKey, trafficType, attributes }).client;
Expand Down
6 changes: 6 additions & 0 deletions src/useSplitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export const DEFAULT_UPDATE_OPTIONS = {
* It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components.
*
* @return A Split Context object
*
* @example
* ```js
* const { factory, client, isReady, isReadyFromCache, hasTimedout, lastUpdate } = useSplitClient({ splitKey: 'user_id' });
* ```
*
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
*/
export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues {
Expand Down
10 changes: 8 additions & 2 deletions src/useSplitTreatments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'
import { useSplitClient } from './useSplitClient';

/**
* 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments).
* 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property object that contains feature flag evaluations.
* It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method.
*
* @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist.
* @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist.
*
* @example
* ```js
* const { treatments: { feature_1, feature_2 }, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useSplitTreatments({ names: ['feature_1', 'feature_2']});
* ```
*
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
*/
export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps {
Expand Down
2 changes: 2 additions & 0 deletions src/useTreatments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useSplitTreatments } from './useSplitTreatments';
*
* @return A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
*
* @deprecated Replace with the new `useSplitTreatments` hook.
*/
export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig {
return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments;
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus {

// Input validation utils that will be replaced eventually

export function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'split names'): false | string[] {
export function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] {
if (Array.isArray(maybeFeatureFlags) && maybeFeatureFlags.length > 0) {
const validatedArray: string[] = [];
// Remove invalid values
Expand All @@ -125,7 +125,7 @@ export function initAttributes(client: SplitIO.IBrowserClient | null, attributes

const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/;

function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'split name'): false | string {
function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag name'): false | string {
if (maybeFeatureFlag == undefined) {
console.log(`[ERROR] you passed a null or undefined ${item}, ${item} must be a non-empty string.`);
} else if (!isString(maybeFeatureFlag)) {
Expand Down
64 changes: 0 additions & 64 deletions types/SplitClient.d.ts

This file was deleted.

9 changes: 0 additions & 9 deletions types/SplitContext.d.ts

This file was deleted.

Loading