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 CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
2.1.2 (April XX, 2025)
2.2.0 (April 15, 2025)
- Added `updateOnSdkUpdate`, `updateOnSdkReady`, `updateOnSdkReadyFromCache` and `updateOnSdkTimedout` props to the `SplitFactoryProvider` component to overwrite the default value (`true`) of the `updateOnSdk<Event>` options in the `useSplitClient` and `useSplitTreatments` hooks.
- Updated development dependencies to use React v19 and TypeScript v4.5.5 to test compatibility and avoid type conflicts when using the SDK with React v19 types.

2.1.1 (April 9, 2025)
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": "2.1.1",
"version": "2.2.0",
"description": "A React library to easily integrate and use Split JS SDK",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
10 changes: 8 additions & 2 deletions src/SplitFactoryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import { SplitFactory } from '@splitsoftware/splitio/client';
* @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
*/
export function SplitFactoryProvider(props: ISplitFactoryProviderProps) {
const { config, factory: propFactory, attributes } = props;
const {
config, factory: propFactory, attributes,
updateOnSdkReady = true, updateOnSdkReadyFromCache = true, updateOnSdkTimedout = true, updateOnSdkUpdate = true
} = props;

const factory = React.useMemo<undefined | SplitIO.IBrowserSDK & { init?: () => void }>(() => {
return propFactory ?
Expand Down Expand Up @@ -62,7 +65,10 @@ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) {
}, [config, propFactory, factory]);

return (
<SplitContext.Provider value={{ factory, client, ...getStatus(client) }} >
<SplitContext.Provider value={{
factory, client, ...getStatus(client),
updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate
}} >
{props.children}
</SplitContext.Provider>
);
Expand Down
27 changes: 26 additions & 1 deletion src/__tests__/SplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { sdkBrowser } from './testUtils/sdkConfigs';
import { ISplitClientChildProps, ISplitFactoryChildProps } from '../types';
import { SplitFactoryProvider } from '../SplitFactoryProvider';
import { SplitClient } from '../SplitClient';
import { SplitContext } from '../SplitContext';
import { SplitContext, useSplitContext } from '../SplitContext';
import { INITIAL_STATUS, testAttributesBinding, TestComponentProps } from './testUtils/utils';
import { getStatus } from '../utils';
import { EXCEPTION_NO_SFP } from '../constants';
Expand Down Expand Up @@ -360,6 +360,31 @@ describe('SplitClient', () => {
testAttributesBinding(Component);
});


test('must overwrite `updateOn<Event>` options in context', () => {
render(
<SplitFactoryProvider updateOnSdkReady={true} updateOnSdkReadyFromCache={false} updateOnSdkTimedout={undefined} >
{React.createElement(() => {
expect(useSplitContext()).toEqual({
...INITIAL_STATUS,
updateOnSdkReadyFromCache: false
});
return null;
})}
<SplitClient updateOnSdkReady={false} updateOnSdkReadyFromCache={undefined} updateOnSdkTimedout={false} >
{React.createElement(() => {
expect(useSplitContext()).toEqual({
...INITIAL_STATUS,
updateOnSdkReady: false,
updateOnSdkReadyFromCache: false,
updateOnSdkTimedout: false
});
return null;
})}
</SplitClient>
</SplitFactoryProvider>
);
});
});

// Tests to validate the migration from `SplitFactoryProvider` with child as a function in v1, to `SplitFactoryProvider` + `SplitClient` with child as a function in v2.
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/SplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,33 +385,33 @@ describe.each([
});

expect(renderTimesComp1).toBe(2);
expect(renderTimesComp2).toBe(2); // updateOnSdkReadyFromCache === false, in second component
expect(renderTimesComp2).toBe(1); // updateOnSdkReadyFromCache === false, in second component

act(() => {
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT);
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT);
});

expect(renderTimesComp1).toBe(3);
expect(renderTimesComp2).toBe(3);
expect(renderTimesComp2).toBe(2);

act(() => {
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY);
});

expect(renderTimesComp1).toBe(3); // updateOnSdkReady === false, in first component
expect(renderTimesComp2).toBe(4);
expect(renderTimesComp2).toBe(3);

act(() => {
(outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE);
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE);
});

expect(renderTimesComp1).toBe(4);
expect(renderTimesComp2).toBe(5);
expect(renderTimesComp2).toBe(4);
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(3); // renderTimes - 1, for the 1st render where SDK is not operational
expect(outerFactory.client('user2').getTreatmentsWithConfig).toBeCalledTimes(4); // idem
expect(outerFactory.client('user2').getTreatmentsWithConfig).toBeCalledTimes(3); // idem
});

it('rerenders and re-evaluates feature flags if client attributes changes.', (done) => {
Expand Down
8 changes: 6 additions & 2 deletions src/__tests__/testUtils/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { ISplitStatus } from '../../types';
import { ISplitStatus, IUpdateProps } from '../../types';
const { SplitFactory: originalSplitFactory } = jest.requireActual('@splitsoftware/splitio/client');

export interface TestComponentProps {
Expand Down Expand Up @@ -116,11 +116,15 @@ export function testAttributesBinding(Component: React.FunctionComponent<TestCom
wrapper.rerender(<Component splitKey={undefined} attributesFactory={undefined} attributesClient={undefined} testSwitch={attributesBindingSwitch} factory={factory} />);
}

export const INITIAL_STATUS: ISplitStatus = {
export const INITIAL_STATUS: ISplitStatus & IUpdateProps = {
isReady: false,
isReadyFromCache: false,
isTimedout: false,
hasTimedout: false,
lastUpdate: 0,
isDestroyed: false,
updateOnSdkReady: true,
updateOnSdkReadyFromCache: true,
updateOnSdkTimedout: true,
updateOnSdkUpdate: true,
}
27 changes: 24 additions & 3 deletions src/__tests__/useSplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ describe('useSplitClient', () => {

act(() => mainClient.__emitter__.emit(Event.SDK_READY_TIMED_OUT)); // do not trigger re-render because updateOnSdkTimedout is false
expect(rendersCount).toBe(1);
expect(currentStatus).toMatchObject(INITIAL_STATUS);
expect(currentStatus).toMatchObject({ ...INITIAL_STATUS, updateOnSdkUpdate: false, updateOnSdkTimedout: false });

wrapper.rerender(<Component updateOnSdkUpdate={false} updateOnSdkTimedout={false} />);
expect(rendersCount).toBe(2);
Expand All @@ -263,7 +263,7 @@ describe('useSplitClient', () => {

wrapper.rerender(<Component updateOnSdkUpdate={false} updateOnSdkTimedout={false} />); // should not update the status (SDK_UPDATE event should be ignored)
expect(rendersCount).toBe(6);
expect(currentStatus).toEqual(previousStatus);
expect(currentStatus).toEqual({ ...previousStatus, updateOnSdkTimedout: false });

wrapper.rerender(<Component updateOnSdkUpdate={null /** invalid type should default to `true` */} />); // trigger re-render and update the status because updateOnSdkUpdate is true and there was an SDK_UPDATE event
expect(rendersCount).toBe(8); // @TODO optimize `useSplitClient` to avoid double render
Expand All @@ -275,10 +275,31 @@ describe('useSplitClient', () => {

wrapper.rerender(<Component updateOnSdkUpdate={false} />);
expect(rendersCount).toBe(10);
expect(currentStatus).toEqual(previousStatus);
expect(currentStatus).toEqual({ ...previousStatus, updateOnSdkUpdate: false });

act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now
expect(rendersCount).toBe(10);
});

test('must prioritize explicitly provided `updateOn<Event>` options over context defaults', () => {
render(
<SplitFactoryProvider updateOnSdkReady={true} updateOnSdkReadyFromCache={false} updateOnSdkTimedout={undefined} >
{React.createElement(() => {
expect(useSplitClient()).toEqual({
...INITIAL_STATUS,
updateOnSdkReadyFromCache: false
});

expect(useSplitClient({ updateOnSdkReady: false, updateOnSdkReadyFromCache: undefined, updateOnSdkTimedout: false })).toEqual({
...INITIAL_STATUS,
updateOnSdkReady: false,
updateOnSdkReadyFromCache: false,
updateOnSdkTimedout: false
});
return null;
})}
</SplitFactoryProvider>
);
});

});
6 changes: 4 additions & 2 deletions src/__tests__/useSplitManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ describe('useSplitManager', () => {
);

expect(hookResult).toStrictEqual({
...INITIAL_STATUS,
manager: outerFactory.manager(),
client: outerFactory.client(),
factory: outerFactory,
...INITIAL_STATUS,
});

act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY));

expect(hookResult).toStrictEqual({
...INITIAL_STATUS,
manager: outerFactory.manager(),
client: outerFactory.client(),
factory: outerFactory,
Expand Down Expand Up @@ -80,16 +81,17 @@ describe('useSplitManager', () => {
);

expect(hookResult).toStrictEqual({
...INITIAL_STATUS,
manager: outerFactory.manager(),
client: outerFactory.client(),
factory: outerFactory,
...INITIAL_STATUS,
});

act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY));
// act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY));

expect(hookResult).toStrictEqual({
...INITIAL_STATUS,
manager: outerFactory.manager(),
client: outerFactory.client(),
factory: outerFactory,
Expand Down
8 changes: 2 additions & 6 deletions src/__tests__/withSplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ jest.mock('@splitsoftware/splitio/client', () => {
});
import { SplitFactory } from '@splitsoftware/splitio/client';
import { sdkBrowser } from './testUtils/sdkConfigs';
import { INITIAL_STATUS } from './testUtils/utils';

/** Test target */
import { withSplitFactory } from '../withSplitFactory';
Expand All @@ -32,15 +33,10 @@ describe('withSplitTreatments', () => {
expect((clientMock.getTreatmentsWithConfig as jest.Mock).mock.calls.length).toBe(0);

expect(props).toStrictEqual({
...INITIAL_STATUS,
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;
Expand Down
72 changes: 35 additions & 37 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,73 +34,71 @@ export interface ISplitStatus {
isDestroyed: boolean;

/**
* Indicates when was the last status event, either `SDK_READY`, `SDK_READY_FROM_CACHE`, `SDK_READY_TIMED_OUT` or `SDK_UPDATE`.
* `lastUpdate` indicates the timestamp of the most recent status event. This timestamp is only updated for events that are being listened to,
* configured via the `updateOnSdkReady` option for `SDK_READY` event, `updateOnSdkReadyFromCache` for `SDK_READY_FROM_CACHE` event,
* `updateOnSdkTimedout` for `SDK_READY_TIMED_OUT` event, and `updateOnSdkUpdate` for `SDK_UPDATE` event.
*/
lastUpdate: number;
}

/**
* Split Context Value interface. It is used to define the value types of Split Context
*/
export interface ISplitContextValues extends ISplitStatus {

/**
* Split factory instance.
*
* NOTE: This property is available for accessing factory methods not covered by the library hooks,
* such as Logging configuration and User Consent.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#logging}),
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#user-consent}
*/
factory?: SplitIO.IBrowserSDK;

/**
* Split client instance.
*
* NOTE: This property is not recommended for direct use, as better alternatives are available:
* - `useSplitTreatments` hook to evaluate feature flags.
* - `useTrack` hook to track events.
*
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
*/
client?: SplitIO.IBrowserClient;
}

/**
* Update Props interface. It defines the props used to configure what SDK events are listened to update the component.
*/
export interface IUpdateProps {

/**
* `updateOnSdkUpdate` indicates if the component will update (i.e., re-render) in case of an `SDK_UPDATE` event.
* If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_UPDATE`.
* `updateOnSdkUpdate` indicates if the hook or component will update (i.e., re-render) or not in case of an `SDK_UPDATE` event.
* It's value is `true` by default.
*/
updateOnSdkUpdate?: boolean;

/**
* `updateOnSdkTimedout` indicates if the component will update (i.e., re-render) in case of a `SDK_READY_TIMED_OUT` event.
* If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY_TIMED_OUT`.
* `updateOnSdkTimedout` indicates if the hook or component will update (i.e., re-render) or not in case of a `SDK_READY_TIMED_OUT` event.
* It's value is `true` by default.
*/
updateOnSdkTimedout?: boolean;

/**
* `updateOnSdkReady` indicates if the component will update (i.e., re-render) in case of a `SDK_READY` event.
* If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY`.
* `updateOnSdkReady` indicates if the hook or component will update (i.e., re-render) or not in case of a `SDK_READY` event.
* It's value is `true` by default.
*/
updateOnSdkReady?: boolean;

/**
* `updateOnSdkReadyFromCache` indicates if the component will update (i.e., re-render) in case of a `SDK_READY_FROM_CACHE` event.
* If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY_FROM_CACHE`.
* `updateOnSdkReadyFromCache` indicates if the hook or component will update (i.e., re-render) or not in case of a `SDK_READY_FROM_CACHE` event.
* This params is only relevant when using `'LOCALSTORAGE'` as storage type, since otherwise the event is never emitted.
* It's value is `true` by default.
*/
updateOnSdkReadyFromCache?: boolean;
}

/**
* Split Context Value interface. It is used to define the value types of Split Context
*/
export interface ISplitContextValues extends ISplitStatus, IUpdateProps {

/**
* Split factory instance.
*
* NOTE: This property is available for accessing factory methods not covered by the library hooks,
* such as Logging configuration and User Consent.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#logging}),
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#user-consent}
*/
factory?: SplitIO.IBrowserSDK;

/**
* Split client instance.
*
* NOTE: This property is not recommended for direct use, as better alternatives are available:
* - `useSplitTreatments` hook to evaluate feature flags.
* - `useTrack` hook to track events.
*
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
*/
client?: SplitIO.IBrowserClient;
}

/**
* Props interface for components wrapped by the `withSplitFactory` HOC. These props are provided by the HOC to the wrapped component.
*
Expand All @@ -112,7 +110,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { }
* SplitFactoryProvider Props interface. These are the props accepted by the `SplitFactoryProvider` component,
* used to instantiate a factory and provide it to the Split Context.
*/
export interface ISplitFactoryProviderProps {
export interface ISplitFactoryProviderProps extends IUpdateProps {

/**
* Config object used to instantiate a Split factory.
Expand Down
Loading