diff --git a/CHANGES.txt b/CHANGES.txt index 38b897c..de08e24 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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` 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) diff --git a/package-lock.json b/package-lock.json index 2caa447..8f93d05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "2.1.1", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "2.1.1", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "11.2.0", diff --git a/package.json b/package.json index f0f5cbf..2282f56 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx index 219a0f3..4d2d26e 100644 --- a/src/SplitFactoryProvider.tsx +++ b/src/SplitFactoryProvider.tsx @@ -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 void }>(() => { return propFactory ? @@ -62,7 +65,10 @@ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) { }, [config, propFactory, factory]); return ( - + {props.children} ); diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 19fbb06..2d1bf1b 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -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'; @@ -360,6 +360,31 @@ describe('SplitClient', () => { testAttributesBinding(Component); }); + + test('must overwrite `updateOn` options in context', () => { + render( + + {React.createElement(() => { + expect(useSplitContext()).toEqual({ + ...INITIAL_STATUS, + updateOnSdkReadyFromCache: false + }); + return null; + })} + + {React.createElement(() => { + expect(useSplitContext()).toEqual({ + ...INITIAL_STATUS, + updateOnSdkReady: false, + updateOnSdkReadyFromCache: false, + updateOnSdkTimedout: false + }); + return null; + })} + + + ); + }); }); // Tests to validate the migration from `SplitFactoryProvider` with child as a function in v1, to `SplitFactoryProvider` + `SplitClient` with child as a function in v2. diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 4ff0a78..cf21ea4 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -385,7 +385,7 @@ 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); @@ -393,7 +393,7 @@ describe.each([ }); expect(renderTimesComp1).toBe(3); - expect(renderTimesComp2).toBe(3); + expect(renderTimesComp2).toBe(2); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); @@ -401,7 +401,7 @@ describe.each([ }); 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); @@ -409,9 +409,9 @@ describe.each([ }); 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) => { diff --git a/src/__tests__/testUtils/utils.tsx b/src/__tests__/testUtils/utils.tsx index 027655e..7571873 100644 --- a/src/__tests__/testUtils/utils.tsx +++ b/src/__tests__/testUtils/utils.tsx @@ -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 { @@ -116,11 +116,15 @@ export function testAttributesBinding(Component: React.FunctionComponent); } -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, } diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index c5ea38f..ea56ad9 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -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(); expect(rendersCount).toBe(2); @@ -263,7 +263,7 @@ describe('useSplitClient', () => { wrapper.rerender(); // 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(); // 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 @@ -275,10 +275,31 @@ describe('useSplitClient', () => { wrapper.rerender(); 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` options over context defaults', () => { + render( + + {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; + })} + + ); + }); + }); diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index a17d19c..9707042 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -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, @@ -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, diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index f11f3a1..af178f2 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -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'; @@ -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; diff --git a/src/types.ts b/src/types.ts index 6fb4f8a..96836c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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. * @@ -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. diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index d9344af..7d78b91 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -3,13 +3,6 @@ import { useSplitContext } from './SplitContext'; import { getSplitClient, initAttributes, getStatus } from './utils'; import { ISplitContextValues, IUseSplitClientOptions } from './types'; -export const DEFAULT_UPDATE_OPTIONS = { - updateOnSdkUpdate: true, - updateOnSdkTimedout: true, - updateOnSdkReady: true, - updateOnSdkReadyFromCache: true, -}; - /** * `useSplitClient` is a hook that returns an Split Context object with the client and its status corresponding to the provided key. * @@ -23,13 +16,16 @@ export const DEFAULT_UPDATE_OPTIONS = { * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues { - const { - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, attributes - } = { ...DEFAULT_UPDATE_OPTIONS, ...options }; - +export function useSplitClient(options: IUseSplitClientOptions = {}): ISplitContextValues { const context = useSplitContext(); const { client: contextClient, factory } = context; + const { + splitKey, attributes, + updateOnSdkReady = context.updateOnSdkReady, + updateOnSdkReadyFromCache = context.updateOnSdkReadyFromCache, + updateOnSdkTimedout = context.updateOnSdkTimedout, + updateOnSdkUpdate = context.updateOnSdkUpdate + } = options; // @TODO Move `getSplitClient` side effects and reduce the function cognitive complexity // @TODO Once `SplitClient` is removed, which updates the context, simplify next line as `const client = factory ? getSplitClient(factory, splitKey) : undefined;` @@ -83,6 +79,6 @@ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextV }, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, status]); return { - factory, client, ...status + factory, client, ...status, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate }; }