From 000345c6d4be1c921d64452e21c85c71684bb9fc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Jul 2023 23:30:17 -0300 Subject: [PATCH 01/67] rename getSplitSharedClient to getSplitClient --- src/SplitClient.tsx | 6 +++--- src/SplitFactory.tsx | 4 ++-- src/useSplitClient.ts | 4 ++-- src/utils.ts | 21 ++++++++++----------- types/utils.d.ts | 4 ++-- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 7c53172..cf1bdec 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; import { ERROR_SC_NO_FACTORY } from './constants'; -import { getStatus, getSplitSharedClient, initAttributes } from './utils'; +import { getStatus, getSplitClient, initAttributes } from './utils'; /** * Common component used to handle the status and events of a Split client passed as prop. @@ -145,8 +145,8 @@ export function SplitClient(props: ISplitClientProps) { {(splitContext: ISplitContextValues) => { const { factory } = splitContext; - // getSplitSharedClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT - const client = factory ? getSplitSharedClient(factory, props.splitKey, props.trafficType) : null; + // getSplitClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT + const client = factory ? getSplitClient(factory, props.splitKey, props.trafficType) : null; return ( ); diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx index 6de0019..5460e0e 100644 --- a/src/SplitFactory.tsx +++ b/src/SplitFactory.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SplitComponent } from './SplitClient'; import { ISplitFactoryProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients } from './utils'; +import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils'; /** * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, @@ -54,7 +54,7 @@ export class SplitFactory extends React.Component; + clientInstances: Set; config: SplitIO.IBrowserSettings; } @@ -36,7 +36,7 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC const newFactory = SplitSdk(config, (modules) => { modules.settings.version = VERSION; }) as IFactoryWithClients; - newFactory.sharedClientInstances = new Set(); + newFactory.clientInstances = new Set(); newFactory.config = config; __factories.set(config, newFactory); } @@ -44,22 +44,21 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC } // idempotent operation -export function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation - const client = factory.client(key, trafficType) as IClientWithContext; - if ((factory as IFactoryWithClients).sharedClientInstances) { - (factory as IFactoryWithClients).sharedClientInstances.add(client); + const client = (key ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + if ((factory as IFactoryWithClients).clientInstances) { + (factory as IFactoryWithClients).clientInstances.add(client); } return client; } export function destroySplitFactory(factory: IFactoryWithClients): Promise { - // call destroy of shared clients and main one - const destroyPromises = []; - factory.sharedClientInstances.forEach((client) => destroyPromises.push(client.destroy())); - destroyPromises.push(factory.client().destroy()); + // call destroy of clients + const destroyPromises: Promise[] = []; + factory.clientInstances.forEach((client) => destroyPromises.push(client.destroy())); // remove references to release allocated memory - factory.sharedClientInstances.clear(); + factory.clientInstances.clear(); __factories.delete(factory.config); return Promise.all(destroyPromises); } diff --git a/types/utils.d.ts b/types/utils.d.ts index 71fc810..5bb66dc 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -14,12 +14,12 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { * FactoryWithClientInstances interface. */ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - sharedClientInstances: Set; + clientInstances: Set; config: SplitIO.IBrowserSettings; } export declare const __factories: Map; export declare function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients; -export declare function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext; +export declare function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext; export declare function destroySplitFactory(factory: IFactoryWithClients): Promise; export interface IClientStatus { isReady: boolean; From 6d2e878610255cab7ffa124c8d444de57bb9f524 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 8 Jul 2023 00:01:59 -0300 Subject: [PATCH 02/67] remove asserts on internal event listeners, unit test should not validate internal implementation details and private methods --- src/SplitClient.tsx | 4 +-- src/__tests__/SplitClient.test.tsx | 37 ++++--------------------- src/__tests__/SplitFactory.test.tsx | 16 ++++------- src/__tests__/testUtils/mockSplitSdk.ts | 28 ++----------------- 4 files changed, 15 insertions(+), 70 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index cf1bdec..1bc849f 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -83,7 +83,7 @@ export class SplitComponent extends React.Component { if (this.props.updateOnSdkReady) this.setState({ lastUpdate: Date.now() }); } @@ -112,7 +112,7 @@ export class SplitComponent extends React.Component { return { SplitFactory: mockSdk() }; }); @@ -20,12 +20,10 @@ import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('SplitClient', () => { test('passes no-ready props to the child if client is not ready.', () => { - let clientToCheck; - render( - {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { + {({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { expect(isReady).toBe(false); expect(isReadyFromCache).toBe(false); expect(hasTimedout).toBe(false); @@ -33,17 +31,11 @@ describe('SplitClient', () => { expect(isDestroyed).toBe(false); expect(lastUpdate).toBe(0); - clientToCheck = client; return null; }} ); - - // After component mount, listeners should be attached for all ONCE events when none of them has been emitted yet - expect(clientListenerCount(clientToCheck, Event.SDK_READY)).toBe(1); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_FROM_CACHE)).toBe(1); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_TIMED_OUT)).toBe(1); }); test('passes ready props to the child if client is ready.', async () => { @@ -52,7 +44,6 @@ describe('SplitClient', () => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); await outerFactory.client().ready(); - let clientToCheck; render( @@ -66,17 +57,11 @@ describe('SplitClient', () => { expect(isDestroyed).toBe(false); expect(lastUpdate).toBe(0); - clientToCheck = client; return null; }} ); - - // After component mount, listeners should not be attached for those ONCE events that has been already emitted - expect(clientListenerCount(clientToCheck, Event.SDK_READY)).toBe(0); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_FROM_CACHE)).toBe(0); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_TIMED_OUT)).toBe(0); // this event was not emitted, but will not anyway, since SDK_READY has. }); test('rerender child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events.', async () => { @@ -89,7 +74,7 @@ describe('SplitClient', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { @@ -130,10 +115,6 @@ describe('SplitClient', () => { act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); expect(renderTimes).toBe(5); - - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); }); test('rerender child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { @@ -146,7 +127,7 @@ describe('SplitClient', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { @@ -179,10 +160,6 @@ describe('SplitClient', () => { act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); expect(renderTimes).toBe(3); - - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); }); test('rerender child only on SDK_READY event, as default behaviour.', async () => { @@ -299,10 +276,6 @@ describe('SplitClient', () => { setTimeout(() => { expect(renderTimes).toBe(6); - // check that outerFactory's clients have no event listeners - // eslint-disable-next-line no-use-before-define - wrapper.unmount(); - assertNoListeners(outerFactory); done(); }); }); @@ -359,7 +332,7 @@ describe('SplitClient', () => { } } - const wrapper = render( + render( diff --git a/src/__tests__/SplitFactory.test.tsx b/src/__tests__/SplitFactory.test.tsx index 68bf307..86a9add 100644 --- a/src/__tests__/SplitFactory.test.tsx +++ b/src/__tests__/SplitFactory.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event, assertNoListeners } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitSdk'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -67,7 +67,7 @@ describe('SplitFactory', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -104,11 +104,8 @@ describe('SplitFactory', () => { act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(5); - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); + expect(renderTimes).toBe(5); }); test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { @@ -116,7 +113,7 @@ describe('SplitFactory', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -146,11 +143,8 @@ describe('SplitFactory', () => { act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(3); - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); + expect(renderTimes).toBe(3); }); test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour.', async () => { diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index 18cfb21..95f0641 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -40,12 +40,6 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { __emitter__.on(Event.SDK_READY, () => { __isReady__ = true; }); __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { __isReadyFromCache__ = true; }); __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; }); - const __internalListenersCount__ = { - [Event.SDK_READY]: 1, - [Event.SDK_READY_FROM_CACHE]: 1, - [Event.SDK_READY_TIMED_OUT]: 1, - [Event.SDK_UPDATE]: 0, - }; // Client methods const track: jest.Mock = jest.fn(() => { @@ -66,9 +60,9 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { const ready: jest.Mock = jest.fn(() => { return new Promise((res, rej) => { if (__isReady__) res(); - else { __internalListenersCount__[Event.SDK_READY]++; __emitter__.on(Event.SDK_READY, res); } + else { __emitter__.on(Event.SDK_READY, res); } if (__hasTimedout__) rej(); - else { __internalListenersCount__[Event.SDK_READY_TIMED_OUT]++; __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } + else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } }); }); const __getStatus = () => ({ @@ -80,6 +74,7 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { }); const destroy: jest.Mock = jest.fn(() => { __isDestroyed__ = true; + // __emitter__.removeAllListeners(); return Promise.resolve(); }); @@ -94,8 +89,6 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { getAttributes, // EventEmitter exposed to trigger events manually __emitter__, - // Count of internal listeners set by the client mock, used to assert how many external listeners were attached - __internalListenersCount__, // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) __getStatus, // Restore the mock client to its initial NO-READY status. @@ -140,18 +133,3 @@ export function mockSdk() { }); } - -export function assertNoListeners(factory: any) { - Object.values((factory as any).__clients__).forEach((client) => assertNoListenersOnClient(client)); -} - -function assertNoListenersOnClient(client: any) { - expect(client.__emitter__.listenerCount(Event.SDK_READY)).toBe(client.__internalListenersCount__[Event.SDK_READY]); - expect(client.__emitter__.listenerCount(Event.SDK_READY_FROM_CACHE)).toBe(client.__internalListenersCount__[Event.SDK_READY_FROM_CACHE]); - expect(client.__emitter__.listenerCount(Event.SDK_READY_TIMED_OUT)).toBe(client.__internalListenersCount__[Event.SDK_READY_TIMED_OUT]); - expect(client.__emitter__.listenerCount(Event.SDK_UPDATE)).toBe(client.__internalListenersCount__[Event.SDK_UPDATE]); -} - -export function clientListenerCount(client: any, event: string) { - return client.__emitter__.listenerCount(event) - client.__internalListenersCount__[event]; -} From 2d04ca800da420edd3b634e0517fd19428552a4a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 8 Jul 2023 00:38:28 -0300 Subject: [PATCH 03/67] Updated getSplitClient util to set client lastUpdate timestamp, and updated SplitComponent to use it. The client lastUpdate timestamp is required to avoid duplicating updates when using useSplitClient or nested SplitClient components --- src/SplitClient.tsx | 12 +++++------ src/__tests__/SplitClient.test.tsx | 7 ++---- src/__tests__/SplitFactory.test.tsx | 7 ++---- src/types.ts | 2 +- src/utils.ts | 33 ++++++++++++++++++----------- types/SplitClient.d.ts | 3 ++- types/types.d.ts | 3 +-- types/utils.d.ts | 11 +++------- 8 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 1bc849f..f5ac63e 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; import { ERROR_SC_NO_FACTORY } from './constants'; -import { getStatus, getSplitClient, initAttributes } from './utils'; +import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils'; /** * Common component used to handle the status and events of a Split client passed as prop. @@ -58,7 +58,6 @@ export class SplitComponent extends React.Component { - if (this.props.updateOnSdkReady) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkReady) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setReadyFromCache = () => { - if (this.props.updateOnSdkReadyFromCache) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkReadyFromCache) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setTimedout = () => { - if (this.props.updateOnSdkTimedout) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkTimedout) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setUpdate = () => { - if (this.props.updateOnSdkUpdate) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkUpdate) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } componentDidMount() { diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 7cfa312..073a4ee 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -99,8 +99,7 @@ describe('SplitClient', () => { fail('Child must not be rerendered'); } expect(client).toBe(outerFactory.client('user2')); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -146,8 +145,7 @@ describe('SplitClient', () => { fail('Child must not be rerendered'); } expect(client).toBe(outerFactory.client('user2')); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -189,7 +187,6 @@ describe('SplitClient', () => { } expect(client).toBe(outerFactory.client('user2')); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); renderTimes++; previousLastUpdate = lastUpdate; return null; diff --git a/src/__tests__/SplitFactory.test.tsx b/src/__tests__/SplitFactory.test.tsx index 86a9add..b5a90e6 100644 --- a/src/__tests__/SplitFactory.test.tsx +++ b/src/__tests__/SplitFactory.test.tsx @@ -91,8 +91,7 @@ describe('SplitFactory', () => { fail('Child must not be rerendered'); } expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -131,8 +130,7 @@ describe('SplitFactory', () => { fail('Child must not be rerendered'); } expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -168,7 +166,6 @@ describe('SplitFactory', () => { } expect(factory).toBe(outerFactory); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); renderTimes++; previousLastUpdate = lastUpdate; return null; diff --git a/src/types.ts b/src/types.ts index d716fb7..5606976 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import SplitIO from '@splitsoftware/splitio/types/splitio'; /** * Split Status interface. It represents the current readiness state of the SDK. */ -interface ISplitStatus { +export interface ISplitStatus { /** * isReady indicates if the Split SDK client has triggered an SDK_READY event and thus is ready to be consumed. diff --git a/src/utils.ts b/src/utils.ts index 0353874..f913d30 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import React from 'react'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { VERSION } from './constants'; +import { ISplitStatus } from './types'; // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. @@ -15,6 +16,7 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { hasTimedout: boolean; isDestroyed: boolean; }; + lastUpdate: number; } /** @@ -47,6 +49,21 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation const client = (key ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + + // Handle client lastUpdate + if (client.lastUpdate === undefined) { + const updateLastUpdate = () => { + const lastUpdate = Date.now(); + client.lastUpdate = lastUpdate > client.lastUpdate ? lastUpdate : lastUpdate === client.lastUpdate ? lastUpdate + 1 : client.lastUpdate + 1; + } + + client.lastUpdate = 0; + client.on(client.Event.SDK_READY, updateLastUpdate); + client.on(client.Event.SDK_READY_FROM_CACHE, updateLastUpdate); + client.on(client.Event.SDK_READY_TIMED_OUT, updateLastUpdate); + client.on(client.Event.SDK_UPDATE, updateLastUpdate); + } + if ((factory as IFactoryWithClients).clientInstances) { (factory as IFactoryWithClients).clientInstances.add(client); } @@ -63,18 +80,9 @@ export function destroySplitFactory(factory: IFactoryWithClients): Promise JSX.Element | null); } -export {}; diff --git a/types/utils.d.ts b/types/utils.d.ts index 5bb66dc..b6db222 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,3 +1,4 @@ +import { ISplitStatus } from './types'; /** * ClientWithContext interface. */ @@ -9,6 +10,7 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { hasTimedout: boolean; isDestroyed: boolean; }; + lastUpdate: number; } /** * FactoryWithClientInstances interface. @@ -21,14 +23,7 @@ export declare const __factories: Map; -export interface IClientStatus { - isReady: boolean; - isReadyFromCache: boolean; - hasTimedout: boolean; - isTimedout: boolean; - isDestroyed: boolean; -} -export declare function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus; +export declare function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus; /** * Checks if React.useContext is available, and logs given message if not * From 67aeb8bac0bf713c2c416011d560bae9d3fcaf2f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 8 Jul 2023 03:08:11 -0300 Subject: [PATCH 04/67] fix useSplitClient to update when the client is ready or ready from cache --- src/__tests__/testUtils/mockSplitSdk.ts | 8 ++- src/__tests__/useSplitClient.test.tsx | 73 +++++++++++++++++++++++++ src/useSplitClient.ts | 56 +++++++++++++++++-- 3 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/useSplitClient.test.tsx diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index 18cfb21..b3c70f1 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -47,6 +47,8 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { [Event.SDK_UPDATE]: 0, }; + let attributesCache = {}; + // Client methods const track: jest.Mock = jest.fn(() => { return true; @@ -54,14 +56,16 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { const getTreatmentsWithConfig: jest.Mock = jest.fn(() => { return 'getTreatmentsWithConfig'; }); - const setAttributes: jest.Mock = jest.fn(() => { + const setAttributes: jest.Mock = jest.fn((attributes) => { + attributesCache = Object.assign(attributesCache, attributes); return true; }); const clearAttributes: jest.Mock = jest.fn(() => { + attributesCache = {}; return true; }); const getAttributes: jest.Mock = jest.fn(() => { - return true; + return attributesCache; }); const ready: jest.Mock = jest.fn(() => { return new Promise((res, rej) => { diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx new file mode 100644 index 0000000..4b46df9 --- /dev/null +++ b/src/__tests__/useSplitClient.test.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; + +/** Mocks */ +import { mockSdk, Event } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; + +/** Test target */ +import { SplitFactory } from '../SplitFactory'; +import { useSplitClient } from '../useSplitClient'; +import { SplitClient } from '../SplitClient'; +import { SplitContext } from '../SplitContext'; + +test('useSplitClient', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitClient++; return null }} + + + {() => { countSplitClientUser2++; return null }} + + {React.createElement(() => { + const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' }); + expect(client).toBe(mainClient); // Assert that the main client was retrieved. + expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. + countUseSplitClient++; + return null; + })} + {React.createElement(() => { + const { client } = useSplitClient('user_2'); + expect(client).toBe(user2Client); + countUseSplitClientUser2++; + 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)); + + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); + + // If SplitClient and useSplitClient retrieve the same client than the context and have default update options, + // they render when the context renders. + expect(countSplitClient).toEqual(countSplitContext); + expect(countUseSplitClient).toEqual(countSplitContext); + + // If SplitClient and useSplitClient retrieve a different client than the context and have default update options, + // they render when the context renders and when the new client is ready and ready from cache. + expect(countSplitClientUser2).toEqual(countSplitContext + 2); + expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); +}); diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5b002e9..fd97122 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,9 +1,16 @@ import React from 'react'; import { SplitContext, INITIAL_CONTEXT } from './SplitContext'; import { ERROR_UC_NO_USECONTEXT } from './constants'; -import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext } from './utils'; +import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext, getStatus } from './utils'; import { ISplitContextValues } from './types'; +const options = { + updateOnSdkUpdate: false, + updateOnSdkTimedout: false, + 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 and trafficType. * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. @@ -15,12 +22,53 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att if (!checkHooks(ERROR_UC_NO_USECONTEXT)) return INITIAL_CONTEXT; const context = React.useContext(SplitContext); - let { factory, client } = context; + const { client: contextClient, factory} = context; + + if (!factory) return context; + + let client = contextClient as IClientWithContext; if (key && factory) { client = getSplitSharedClient(factory, key, trafficType); } initAttributes(client, attributes); - return client === context.client ? context : { - ...context, client, ...(client as IClientWithContext).__getStatus() + + const [lastUpdate, setLastUpdate] = React.useState(client === contextClient ? context.lastUpdate : 0); + + // Handle client events + React.useEffect(() => { + const setReady = () => { + if (options.updateOnSdkReady) setLastUpdate(Date.now()); + } + + const setReadyFromCache = () => { + if (options.updateOnSdkReadyFromCache) setLastUpdate(Date.now()); + } + + const setTimedout = () => { + if (options.updateOnSdkTimedout) setLastUpdate(Date.now()); + } + + const setUpdate = () => { + if (options.updateOnSdkUpdate) setLastUpdate(Date.now()); + } + + // Subscribe to SDK events + const status = getStatus(client); + if (!status.isReady) client.once(client.Event.SDK_READY, setReady); + if (!status.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache); + if (!status.hasTimedout && !status.isReady) client.once(client.Event.SDK_READY_TIMED_OUT, setTimedout); + client.on(client.Event.SDK_UPDATE, setUpdate); + + return () => { + // Unsubscribe from events + client.off(client!.Event.SDK_READY, setReady); + client.off(client!.Event.SDK_READY_FROM_CACHE, setReadyFromCache); + client.off(client!.Event.SDK_READY_TIMED_OUT, setTimedout); + client.off(client!.Event.SDK_UPDATE, setUpdate); + } + }, [client]); + + return { + factory, client, ...getStatus(client), lastUpdate }; } From c002bf641cd93f6bc736914865efb836ce771c50 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 8 Jul 2023 03:14:09 -0300 Subject: [PATCH 05/67] support update options in useSplitClient and useSplitTreatments --- src/__tests__/useSplitClient.test.tsx | 57 +++++++++++++++++++++++++++ src/useSplitClient.ts | 10 +++-- src/useSplitTreatments.ts | 6 +-- types/useSplitClient.d.ts | 4 +- types/useSplitTreatments.d.ts | 4 +- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 4b46df9..cc8b87b 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -21,6 +21,8 @@ test('useSplitClient', () => { const user2Client = outerFactory.client('user_2') as any; let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; + let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0; + let countNestedComponent = 0; render( @@ -47,6 +49,49 @@ test('useSplitClient', () => { countUseSplitClientUser2++; return null; })} + + {() => { countSplitClientWithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, undefined, { updateOnSdkUpdate: true }).client; + countUseSplitClientWithUpdate++; + return null; + })} + + {() => { countSplitClientUser2WithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true }); + countUseSplitClientUser2WithUpdate++; + return null; + })} + + {React.createElement(() => { + const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true }); + countNestedComponent++; + + expect(status.client).toBe(user2Client); + switch (countNestedComponent) { + case 1: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(false); + break; + case 2: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(true); + break; + case 3: + expect(status.isReady).toBe(true); + expect(status.isReadyFromCache).toBe(true); + break; + case 4: + break; + default: + throw new Error('Unexpected render'); + } + return null; + })} + ); @@ -70,4 +115,16 @@ test('useSplitClient', () => { // they render when the context renders and when the new client is ready and ready from cache. expect(countSplitClientUser2).toEqual(countSplitContext + 2); expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); + + // If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the client updates. + expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1); + expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1); + + // If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the new client is ready, ready from cache and updates. + expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + + expect(countNestedComponent).toEqual(4); }); diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index fd97122..c5e9b60 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -2,9 +2,9 @@ import React from 'react'; import { SplitContext, INITIAL_CONTEXT } from './SplitContext'; import { ERROR_UC_NO_USECONTEXT } from './constants'; import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext, getStatus } from './utils'; -import { ISplitContextValues } from './types'; +import { ISplitContextValues, IUpdateProps } from './types'; -const options = { +const DEFAULT_OPTIONS = { updateOnSdkUpdate: false, updateOnSdkTimedout: false, updateOnSdkReady: true, @@ -18,11 +18,13 @@ const options = { * @return A Split Context object * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues { +export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options: IUpdateProps = {}): ISplitContextValues { if (!checkHooks(ERROR_UC_NO_USECONTEXT)) return INITIAL_CONTEXT; + options = { ...DEFAULT_OPTIONS, ...options }; + const context = React.useContext(SplitContext); - const { client: contextClient, factory} = context; + const { client: contextClient, factory } = context; if (!factory) return context; diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 14cab24..a090f6c 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,6 +1,6 @@ import { getControlTreatmentsWithConfig, ERROR_UT_NO_USECONTEXT } from './constants'; import { checkHooks, IClientWithContext } from './utils'; -import { ISplitTreatmentsChildProps } from './types'; +import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; import { INITIAL_CONTEXT } from './SplitContext'; import { useSplitClient } from './useSplitClient'; @@ -11,8 +11,8 @@ import { useSplitClient } from './useSplitClient'; * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ -export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps { - const context = checkHooks(ERROR_UT_NO_USECONTEXT) ? useSplitClient(key) : INITIAL_CONTEXT; +export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps { + const context = checkHooks(ERROR_UT_NO_USECONTEXT) ? useSplitClient(key, undefined, undefined, options) : INITIAL_CONTEXT; const client = context.client; const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? client.getTreatmentsWithConfig(splitNames, attributes) : diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts index 64297af..db33202 100644 --- a/types/useSplitClient.d.ts +++ b/types/useSplitClient.d.ts @@ -1,4 +1,4 @@ -import { ISplitContextValues } from './types'; +import { ISplitContextValues, IUpdateProps } from './types'; /** * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. @@ -6,4 +6,4 @@ import { ISplitContextValues } from './types'; * @return A Split Context object * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues; +export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues; diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts index b83b813..edbda7f 100644 --- a/types/useSplitTreatments.d.ts +++ b/types/useSplitTreatments.d.ts @@ -1,4 +1,4 @@ -import { ISplitTreatmentsChildProps } from './types'; +import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; /** * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. @@ -6,4 +6,4 @@ import { ISplitTreatmentsChildProps } from './types'; * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ -export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps; +export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps; From 23bd2eaec8dd9ef46a0eb84e660e067bb265279f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 12:14:24 -0300 Subject: [PATCH 06/67] removed unnecessary non-null assertion operator --- src/useSplitClient.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index fd97122..8af3a11 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -61,10 +61,10 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att return () => { // Unsubscribe from events - client.off(client!.Event.SDK_READY, setReady); - client.off(client!.Event.SDK_READY_FROM_CACHE, setReadyFromCache); - client.off(client!.Event.SDK_READY_TIMED_OUT, setTimedout); - client.off(client!.Event.SDK_UPDATE, setUpdate); + client.off(client.Event.SDK_READY, setReady); + client.off(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache); + client.off(client.Event.SDK_READY_TIMED_OUT, setTimedout); + client.off(client.Event.SDK_UPDATE, setUpdate); } }, [client]); From 172a78bc05b6a6ad79767c7eb9c55ca7fbc44eb4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 12:27:00 -0300 Subject: [PATCH 07/67] added unit test for useSplitTreatments --- src/__tests__/useSplitTreatments.test.tsx | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/__tests__/useSplitTreatments.test.tsx diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx new file mode 100644 index 0000000..a51e7f0 --- /dev/null +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; + +/** Mocks */ +import { mockSdk, Event } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; + +/** Test target */ +import { SplitFactory } from '../SplitFactory'; +import { useSplitTreatments } from '../useSplitTreatments'; +import { SplitTreatments } from '../SplitTreatments'; +import { SplitContext } from '../SplitContext'; + +test('useSplitTreatments', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0; + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitTreatments++; return null }} + + {React.createElement(() => { + const { client } = useSplitTreatments(['split_test'], { att1: 'att1' } ); + expect(client).toBe(mainClient); // Assert that the main client was retrieved. + countUseSplitTreatments++; + return null; + })} + {React.createElement(() => { + const { client } = useSplitTreatments(['split_test'], undefined, 'user_2'); + expect(client).toBe(user2Client); + countUseSplitTreatmentsUser2++; + 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)); + + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); + + // SplitTreatments and useSplitTreatments render when the context renders. + expect(countSplitTreatments).toEqual(countSplitContext); + expect(countUseSplitTreatments).toEqual(countSplitContext); + + // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. + expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); +}); From a9b266259e50f559753afa23bdcaf606d761e31a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 12:45:07 -0300 Subject: [PATCH 08/67] added extra asserts --- src/__tests__/testUtils/mockSplitSdk.ts | 7 +++-- src/__tests__/useSplitTreatments.test.tsx | 33 ++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index b3c70f1..93b7c22 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -53,8 +53,11 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { const track: jest.Mock = jest.fn(() => { return true; }); - const getTreatmentsWithConfig: jest.Mock = jest.fn(() => { - return 'getTreatmentsWithConfig'; + const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => { + return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => { + result[featureName] = { treatment: 'on', config: null }; + return result; + }, {}); }); const setAttributes: jest.Mock = jest.fn((attributes) => { attributesCache = Object.assign(attributesCache, attributes); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index a51e7f0..860c3b4 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -14,6 +14,25 @@ import { SplitFactory } from '../SplitFactory'; import { useSplitTreatments } from '../useSplitTreatments'; import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; +import { ISplitTreatmentsChildProps } from '../types'; + +function validateTreatments({ treatments, isReady, isReadyFromCache}: ISplitTreatmentsChildProps) { + if (isReady || isReadyFromCache) { + expect(treatments).toEqual({ + split_test: { + treatment: 'on', + config: null, + } + }) + } else { + expect(treatments).toEqual({ + split_test: { + treatment: 'control', + config: null, + } + }) + } +} test('useSplitTreatments', () => { const outerFactory = SplitSdk(sdkBrowser); @@ -32,14 +51,16 @@ test('useSplitTreatments', () => { {() => { countSplitTreatments++; return null }} {React.createElement(() => { - const { client } = useSplitTreatments(['split_test'], { att1: 'att1' } ); - expect(client).toBe(mainClient); // Assert that the main client was retrieved. + const context = useSplitTreatments(['split_test'], { att1: 'att1' } ); + expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. + validateTreatments(context); countUseSplitTreatments++; return null; })} {React.createElement(() => { - const { client } = useSplitTreatments(['split_test'], undefined, 'user_2'); - expect(client).toBe(user2Client); + const context = useSplitTreatments(['split_test'], undefined, 'user_2'); + expect(context.client).toBe(user2Client); + validateTreatments(context); countUseSplitTreatmentsUser2++; return null; })} @@ -60,7 +81,11 @@ test('useSplitTreatments', () => { // SplitTreatments and useSplitTreatments render when the context renders. expect(countSplitTreatments).toEqual(countSplitContext); expect(countUseSplitTreatments).toEqual(countSplitContext); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(4); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }); // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(2); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); From 637934aa01170b4e7c40b3f56628146791aba77b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 13:41:59 -0300 Subject: [PATCH 09/67] update unit tests for useSplitTreatments --- src/SplitClient.tsx | 2 +- src/__tests__/useSplitTreatments.test.tsx | 32 +++++++++++++++-------- src/useSplitClient.ts | 1 + 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 7c53172..b05ae03 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -83,7 +83,7 @@ export class SplitComponent extends React.Component { if (this.props.updateOnSdkReady) this.setState({ lastUpdate: Date.now() }); } diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 860c3b4..5cf48ea 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -16,7 +16,7 @@ import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; -function validateTreatments({ treatments, isReady, isReadyFromCache}: ISplitTreatmentsChildProps) { +function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { if (isReady || isReadyFromCache) { expect(treatments).toEqual({ split_test: { @@ -34,12 +34,12 @@ function validateTreatments({ treatments, isReady, isReadyFromCache}: ISplitTrea } } -test('useSplitTreatments', () => { +test('useSplitTreatments', async () => { const outerFactory = SplitSdk(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; - let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0; + let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; render( @@ -51,7 +51,7 @@ test('useSplitTreatments', () => { {() => { countSplitTreatments++; return null }} {React.createElement(() => { - const context = useSplitTreatments(['split_test'], { att1: 'att1' } ); + const context = useSplitTreatments(['split_test'], { att1: 'att1' }); expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. validateTreatments(context); countUseSplitTreatments++; @@ -64,16 +64,24 @@ test('useSplitTreatments', () => { countUseSplitTreatmentsUser2++; return null; })} + {React.createElement(() => { + const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true }); + expect(context.client).toBe(user2Client); + validateTreatments(context); + countUseSplitTreatmentsUser2WithUpdate++; + 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)); + // Awaiting to make sure each event is processed with a different lastUpdate timestamp. + await act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + await act(() => mainClient.__emitter__.emit(Event.SDK_READY)); + await act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); + await act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + await act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + await act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. expect(countSplitContext).toEqual(3); @@ -86,6 +94,8 @@ test('useSplitTreatments', () => { // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); - expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(2); + // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. + expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 26ab544..8ba403a 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -37,6 +37,7 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att const [lastUpdate, setLastUpdate] = React.useState(client === contextClient ? context.lastUpdate : 0); // Handle client events + // NOTE: assuming that SDK events are scattered in time so that Date.now() timestamps are unique per event and trigger an update React.useEffect(() => { const setReady = () => { if (options.updateOnSdkReady) setLastUpdate(Date.now()); From 509ba9a8c619dc02debe09132a9d9d68131d6d69 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 17:07:51 -0300 Subject: [PATCH 10/67] export new hooks. add 'options' param to useClient and useTreatments hooks --- src/__tests__/index.test.ts | 6 ++++++ src/index.ts | 2 ++ src/useClient.ts | 5 +++-- src/useTreatments.ts | 5 +++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 647dbd8..44e98eb 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -11,6 +11,8 @@ import { useManager as exportedUseManager, useTrack as exportedUseTrack, useTreatments as exportedUseTreatments, + useSplitClient as exportedUseSplitClient, + useSplitTreatments as exportedUseSplitTreatments, } from '../index'; import { SplitContext } from '../SplitContext'; import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client'; @@ -24,6 +26,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', () => { @@ -44,6 +48,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', () => { diff --git a/src/index.ts b/src/index.ts index 52a0de8..f6c10a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/useClient.ts b/src/useClient.ts index ddafee0..b9839c9 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -1,3 +1,4 @@ +import { IUpdateProps } from './types'; import { useSplitClient } from './useSplitClient'; /** @@ -8,6 +9,6 @@ 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} */ -export function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { - return useSplitClient(key, trafficType, attributes).client; +export function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): SplitIO.IBrowserClient | null { + return useSplitClient(key, trafficType, attributes, options).client; } diff --git a/src/useTreatments.ts b/src/useTreatments.ts index 4aed7d8..4b90356 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -1,3 +1,4 @@ +import { IUpdateProps } from './types'; import { useSplitTreatments } from './useSplitTreatments'; /** @@ -8,6 +9,6 @@ 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} */ -export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { - return useSplitTreatments(featureFlagNames, attributes, key).treatments; +export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): SplitIO.TreatmentsWithConfig { + return useSplitTreatments(featureFlagNames, attributes, key, options).treatments; } From 9cb112daf587d8a1736205c53d2f70299a4877b9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 17:15:30 -0300 Subject: [PATCH 11/67] update type definitions --- types/index.d.ts | 2 ++ types/useClient.d.ts | 3 ++- types/useTreatments.d.ts | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 91b8d2a..eaa5093 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,4 +9,6 @@ export { useClient } from './useClient'; export { useTreatments } from './useTreatments'; export { useTrack } from './useTrack'; export { useManager } from './useManager'; +export { useSplitClient } from './useSplitClient'; +export { useSplitTreatments } from './useSplitTreatments'; export { SplitContext } from './SplitContext'; diff --git a/types/useClient.d.ts b/types/useClient.d.ts index 75284f7..64517b5 100644 --- a/types/useClient.d.ts +++ b/types/useClient.d.ts @@ -1,3 +1,4 @@ +import { IUpdateProps } from './types'; /** * 'useClient' is a hook that returns a client from the Split context. * It uses the 'useContext' hook to access the context, which is updated by @@ -6,4 +7,4 @@ * @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} */ -export declare function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; +export declare function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): SplitIO.IBrowserClient | null; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts index 6b688e7..7d44ed7 100644 --- a/types/useTreatments.d.ts +++ b/types/useTreatments.d.ts @@ -1,3 +1,4 @@ +import { IUpdateProps } from './types'; /** * 'useTreatments' is a hook that returns an object of feature flag evaluations (i.e., treatments). * It uses the 'useContext' hook to access the client from the Split context, @@ -6,4 +7,4 @@ * @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} */ -export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; +export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): SplitIO.TreatmentsWithConfig; From 44f0d13c4123701842b2de29ea4a8ac67f5dd326 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 17:42:23 -0300 Subject: [PATCH 12/67] optimize evaluation on useSplitTreatments using a memoized version of client.getTreatmentsWithConfig --- src/SplitTreatments.tsx | 20 +++----------------- src/useSplitTreatments.ts | 8 ++++++-- src/utils.ts | 25 ++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 8870e16..563b2cf 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -1,21 +1,8 @@ import React from 'react'; -import memoizeOne from 'memoize-one'; -import shallowEqual from 'shallowequal'; import { SplitContext } from './SplitContext'; import { ISplitTreatmentsProps, ISplitContextValues } from './types'; import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants'; - -function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { - return newArgs[0] === lastArgs[0] && // client - newArgs[1] === lastArgs[1] && // lastUpdate - shallowEqual(newArgs[2], lastArgs[2]) && // names - shallowEqual(newArgs[3], lastArgs[3]) && // attributes - shallowEqual(newArgs[4], lastArgs[4]); // client attributes -} - -function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) { - return client.getTreatmentsWithConfig(names, attributes); -} +import { memoizeGetTreatmentsWithConfig } from './utils'; /** * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to @@ -27,9 +14,8 @@ export class SplitTreatments extends React.Component { private logWarning?: boolean; - // Attaching a memoized `client.getTreatmentsWithConfig` function to the component instance, to avoid duplicated impressions because - // the function result is the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. - private evaluateFeatureFlags = memoizeOne(evaluateFeatureFlags, argsAreEqual); + // Using a memoized `client.getTreatmentsWithConfig` function to avoid duplicated impressions + private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig(); render() { const { names, children, attributes } = this.props; diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index a090f6c..1a8485a 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,5 +1,6 @@ +import React from 'react'; import { getControlTreatmentsWithConfig, ERROR_UT_NO_USECONTEXT } from './constants'; -import { checkHooks, IClientWithContext } from './utils'; +import { checkHooks, IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils'; import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; import { INITIAL_CONTEXT } from './SplitContext'; import { useSplitClient } from './useSplitClient'; @@ -14,8 +15,11 @@ import { useSplitClient } from './useSplitClient'; export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps { const context = checkHooks(ERROR_UT_NO_USECONTEXT) ? useSplitClient(key, undefined, undefined, options) : INITIAL_CONTEXT; const client = context.client; + + const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); + const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - client.getTreatmentsWithConfig(splitNames, attributes) : + getTreatmentsWithConfig(client, context.lastUpdate, splitNames, attributes, { ...client.getAttributes() }) : getControlTreatmentsWithConfig(splitNames); return { diff --git a/src/utils.ts b/src/utils.ts index 50f93f6..66b2f6e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,6 @@ import React from 'react'; +import memoizeOne from 'memoize-one'; +import shallowEqual from 'shallowequal'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { VERSION } from './constants'; @@ -91,7 +93,8 @@ export function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus // Other utils /** - * Checks if React.useContext is available, and logs given message if not + * Checks if React.useContext is available, and logs given message if not. + * Checking for React.useContext is a way to detect if the current React version is >= 16.8.0 and other hooks used by the SDK are available too. * * @param message * @returns boolean indicating if React.useContext is available @@ -170,3 +173,23 @@ function uniq(arr: string[]): string[] { function isString(val: unknown): val is string { return typeof val === 'string' || val instanceof String; } + +/** + * Gets a memoized version of the `client.getTreatmentsWithConfig` method. + * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. + */ +export function memoizeGetTreatmentsWithConfig() { + return memoizeOne(evaluateFeatureFlags, argsAreEqual); +} + +function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { + return newArgs[0] === lastArgs[0] && // client + newArgs[1] === lastArgs[1] && // lastUpdate + shallowEqual(newArgs[2], lastArgs[2]) && // names + shallowEqual(newArgs[3], lastArgs[3]) && // attributes + shallowEqual(newArgs[4], lastArgs[4]); // client attributes +} + +function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) { + return client.getTreatmentsWithConfig(names, attributes); +} From 7f64178885812914064846ad5268e71e967c87f5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 18:02:43 -0300 Subject: [PATCH 13/67] adding a delay on test, to reduce flakiness --- src/__tests__/useSplitTreatments.test.tsx | 16 +++++++++------- types/utils.d.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 5cf48ea..a69394d 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -75,13 +75,15 @@ test('useSplitTreatments', async () => { ); - // Awaiting to make sure each event is processed with a different lastUpdate timestamp. - await act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - await act(() => mainClient.__emitter__.emit(Event.SDK_READY)); - await act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); - await act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - await act(() => user2Client.__emitter__.emit(Event.SDK_READY)); - await act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); + // Adding a delay between events to make sure they are processed with a different lastUpdate timestamp. + act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + await new Promise(resolve => setTimeout(resolve, 10)); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + await new Promise(resolve => setTimeout(resolve, 10)); + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); + act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. expect(countSplitContext).toEqual(3); diff --git a/types/utils.d.ts b/types/utils.d.ts index 71fc810..4a7a503 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -30,7 +30,8 @@ export interface IClientStatus { } export declare function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus; /** - * Checks if React.useContext is available, and logs given message if not + * Checks if React.useContext is available, and logs given message if not. + * Checking for React.useContext is a way to detect if the current React version is >= 16.8.0 and other hooks used by the SDK are available too. * * @param message * @returns boolean indicating if React.useContext is available @@ -41,3 +42,10 @@ export declare function validateFeatureFlags(maybeFeatureFlags: unknown, listNam * Manage client attributes binding */ export declare function initAttributes(client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes): void; +/** + * Gets a memoized version of the `client.getTreatmentsWithConfig` method. + * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. + */ +export declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; +declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; +export {}; From 1239b3e40f26aea346fc0428c9c3869627f14897 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Jul 2023 23:43:51 -0300 Subject: [PATCH 14/67] Rename getSplitSharedClient to getSplitClient, and reuse in SplitFactory to get the main client --- src/SplitClient.tsx | 6 +++--- src/SplitFactory.tsx | 4 ++-- src/__tests__/useSplitClient.test.tsx | 8 +++++--- src/useSplitClient.ts | 4 ++-- src/utils.ts | 19 +++++++++---------- types/utils.d.ts | 4 ++-- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index b05ae03..8c1bc72 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; import { ERROR_SC_NO_FACTORY } from './constants'; -import { getStatus, getSplitSharedClient, initAttributes } from './utils'; +import { getStatus, getSplitClient, initAttributes } from './utils'; /** * Common component used to handle the status and events of a Split client passed as prop. @@ -145,8 +145,8 @@ export function SplitClient(props: ISplitClientProps) { {(splitContext: ISplitContextValues) => { const { factory } = splitContext; - // getSplitSharedClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT - const client = factory ? getSplitSharedClient(factory, props.splitKey, props.trafficType) : null; + // getSplitClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT + const client = factory ? getSplitClient(factory, props.splitKey, props.trafficType) : null; return ( ); diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx index 6de0019..5460e0e 100644 --- a/src/SplitFactory.tsx +++ b/src/SplitFactory.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SplitComponent } from './SplitClient'; import { ISplitFactoryProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients } from './utils'; +import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils'; /** * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, @@ -54,7 +54,7 @@ export class SplitFactory extends React.Component { +test('useSplitClient', async () => { const outerFactory = SplitSdk(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; @@ -97,10 +97,12 @@ test('useSplitClient', () => { ); 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)); + await new Promise(resolve => setTimeout(resolve, 10)); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + await new Promise(resolve => setTimeout(resolve, 10)); + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 8ba403a..318dc92 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,7 +1,7 @@ import React from 'react'; import { SplitContext, INITIAL_CONTEXT } from './SplitContext'; import { ERROR_UC_NO_USECONTEXT } from './constants'; -import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext, getStatus } from './utils'; +import { getSplitClient, checkHooks, initAttributes, IClientWithContext, getStatus } from './utils'; import { ISplitContextValues, IUpdateProps } from './types'; const DEFAULT_OPTIONS = { @@ -30,7 +30,7 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att let client = contextClient as IClientWithContext; if (key && factory) { - client = getSplitSharedClient(factory, key, trafficType); + client = getSplitClient(factory, key, trafficType); } initAttributes(client, attributes); diff --git a/src/utils.ts b/src/utils.ts index 66b2f6e..c9d135d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,7 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { * FactoryWithClientInstances interface. */ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - sharedClientInstances: Set; + clientInstances: Set; config: SplitIO.IBrowserSettings; } @@ -38,7 +38,7 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC const newFactory = SplitSdk(config, (modules) => { modules.settings.version = VERSION; }) as IFactoryWithClients; - newFactory.sharedClientInstances = new Set(); + newFactory.clientInstances = new Set(); newFactory.config = config; __factories.set(config, newFactory); } @@ -46,22 +46,21 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC } // idempotent operation -export function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation - const client = factory.client(key, trafficType) as IClientWithContext; - if ((factory as IFactoryWithClients).sharedClientInstances) { - (factory as IFactoryWithClients).sharedClientInstances.add(client); + const client = (key ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + if ((factory as IFactoryWithClients).clientInstances) { + (factory as IFactoryWithClients).clientInstances.add(client); } return client; } export function destroySplitFactory(factory: IFactoryWithClients): Promise { // call destroy of shared clients and main one - const destroyPromises = []; - factory.sharedClientInstances.forEach((client) => destroyPromises.push(client.destroy())); - destroyPromises.push(factory.client().destroy()); + const destroyPromises: Promise[] = []; + factory.clientInstances.forEach((client) => destroyPromises.push(client.destroy())); // remove references to release allocated memory - factory.sharedClientInstances.clear(); + factory.clientInstances.clear(); __factories.delete(factory.config); return Promise.all(destroyPromises); } diff --git a/types/utils.d.ts b/types/utils.d.ts index 4a7a503..33615de 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -14,12 +14,12 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { * FactoryWithClientInstances interface. */ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - sharedClientInstances: Set; + clientInstances: Set; config: SplitIO.IBrowserSettings; } export declare const __factories: Map; export declare function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients; -export declare function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext; +export declare function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext; export declare function destroySplitFactory(factory: IFactoryWithClients): Promise; export interface IClientStatus { isReady: boolean; From 91e4cc1b99d5541513c8e97f96756d4a1384ad99 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Jul 2023 02:48:03 -0300 Subject: [PATCH 15/67] fix useSplitClient --- src/__tests__/useSplitTreatments.test.tsx | 6 ++++++ src/useSplitClient.ts | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index a69394d..8ed4a9c 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -40,6 +40,8 @@ test('useSplitTreatments', async () => { const user2Client = outerFactory.client('user_2') as any; let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; + const lastUpdateSetUser2 = new Set(); + const lastUpdateSetUser2WithUpdate = new Set(); render( @@ -61,6 +63,7 @@ test('useSplitTreatments', async () => { const context = useSplitTreatments(['split_test'], undefined, 'user_2'); expect(context.client).toBe(user2Client); validateTreatments(context); + lastUpdateSetUser2.add(context.lastUpdate); countUseSplitTreatmentsUser2++; return null; })} @@ -68,6 +71,7 @@ test('useSplitTreatments', async () => { const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true }); expect(context.client).toBe(user2Client); validateTreatments(context); + lastUpdateSetUser2WithUpdate.add(context.lastUpdate); countUseSplitTreatmentsUser2WithUpdate++; return null; })} @@ -96,8 +100,10 @@ test('useSplitTreatments', async () => { // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); + expect(lastUpdateSetUser2.size).toEqual(3); // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); + expect(lastUpdateSetUser2WithUpdate.size).toEqual(4); expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 318dc92..02d4151 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -26,8 +26,6 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att const context = React.useContext(SplitContext); const { client: contextClient, factory } = context; - if (!factory) return context; - let client = contextClient as IClientWithContext; if (key && factory) { client = getSplitClient(factory, key, trafficType); @@ -39,6 +37,8 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att // Handle client events // NOTE: assuming that SDK events are scattered in time so that Date.now() timestamps are unique per event and trigger an update React.useEffect(() => { + if (!client) return; + const setReady = () => { if (options.updateOnSdkReady) setLastUpdate(Date.now()); } @@ -72,6 +72,6 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att }, [client]); return { - factory, client, ...getStatus(client), lastUpdate + factory, client, ...getStatus(client), lastUpdate: client === contextClient ? context.lastUpdate : lastUpdate, }; } From a72c49fb2e15a22f71303112e939ff76b08cae57 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 21 Jul 2023 19:03:56 -0300 Subject: [PATCH 16/67] added eslint-plugin-react-hooks to linter, to enforce rules of hooks --- .eslintrc.js | 3 ++- .github/workflows/ci.yml | 2 +- package-lock.json | 20 ++++++++++++++++++++ package.json | 3 ++- src/__tests__/useClient.test.tsx | 24 +++++++++++++++--------- src/useManager.ts | 3 --- src/useSplitClient.ts | 7 ++----- src/useSplitTreatments.ts | 7 +++---- src/useTrack.ts | 4 +--- src/utils.ts | 18 ------------------ types/utils.d.ts | 7 ------- 11 files changed, 46 insertions(+), 52 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 13d2f3b..cd67531 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,8 @@ module.exports = { 'extends': [ 'eslint:recommended', 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended' + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended' ], 'parser': '@typescript-eslint/parser', 'parserOptions': { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6a4fe1..bc3a8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: npm run test -- --coverage - name: npm build - run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") BUILD_COMMIT=${{ github.sha }} npm run build + run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") npm run build - name: Set VERSION env run: echo "VERSION=$(cat package.json | jq -r .version)" >> $GITHUB_ENV diff --git a/package-lock.json b/package-lock.json index cd658ee..010ce62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "eslint-plugin-compat": "^4.1.2", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", @@ -4146,6 +4147,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -14456,6 +14469,13 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index e7132ad..fcd8f91 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "build:cjs": "rimraf lib/* types/* && tsc -m commonjs --outDir lib -d true --declarationDir types", "build:esm": "rimraf es/* && tsc", - "build:umd": "rimraf umd/* && webpack --config webpack.dev.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT", + "build:umd": "rimraf umd/* && webpack --config webpack.dev.js --env branch=$BUILD_BRANCH && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH", "build": "npm run build:cjs && npm run build:esm && npm run build:umd", "postbuild": "replace 'REACT_SDK_VERSION_NUMBER' $npm_package_version ./lib/constants.js ./es/constants.js ./umd -r", "check": "npm run check:lint && npm run check:types", @@ -81,6 +81,7 @@ "eslint-plugin-compat": "^4.1.2", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", diff --git a/src/__tests__/useClient.test.tsx b/src/__tests__/useClient.test.tsx index 72e3b47..e3a028c 100644 --- a/src/__tests__/useClient.test.tsx +++ b/src/__tests__/useClient.test.tsx @@ -34,13 +34,16 @@ describe('useClient', () => { test('returns the client from the context updated by SplitClient.', () => { const outerFactory = SplitSdk(sdkBrowser); let client; + + const CustomComponent = () => { + client = useClient(); + return null; + } + render( - {React.createElement(() => { - client = useClient(); - return null; - })} + ); @@ -79,14 +82,17 @@ describe('useClient', () => { test('attributes binding test with utility', (done) => { + // eslint-disable-next-line react/prop-types + const InnerComponent = ({ splitKey, attributesClient, testSwitch}) => { + useClient(splitKey, 'user', attributesClient); + testSwitch(done, splitKey); + return null; + }; + function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( - {React.createElement(() => { - useClient(splitKey, 'user', attributesClient); - testSwitch(done, splitKey); - return null; - })} + ); } diff --git a/src/useManager.ts b/src/useManager.ts index 0a9b497..beceb38 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -1,7 +1,5 @@ import React from 'react'; import { SplitContext } from './SplitContext'; -import { ERROR_UM_NO_USECONTEXT } from './constants'; -import { checkHooks } from './utils'; /** * 'useManager' is a hook that returns the Manager instance from the Split factory. @@ -12,7 +10,6 @@ import { checkHooks } from './utils'; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} */ export function useManager(): SplitIO.IManager | null { - if (!checkHooks(ERROR_UM_NO_USECONTEXT)) return null; const { factory } = React.useContext(SplitContext); return factory ? factory.manager() : null; } diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5b002e9..d16dd79 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,7 +1,6 @@ import React from 'react'; -import { SplitContext, INITIAL_CONTEXT } from './SplitContext'; -import { ERROR_UC_NO_USECONTEXT } from './constants'; -import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext } from './utils'; +import { SplitContext } from './SplitContext'; +import { getSplitSharedClient, initAttributes, IClientWithContext } from './utils'; import { ISplitContextValues } from './types'; /** @@ -12,8 +11,6 @@ import { ISplitContextValues } from './types'; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues { - if (!checkHooks(ERROR_UC_NO_USECONTEXT)) return INITIAL_CONTEXT; - const context = React.useContext(SplitContext); let { factory, client } = context; if (key && factory) { diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 14cab24..2cb914d 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,7 +1,6 @@ -import { getControlTreatmentsWithConfig, ERROR_UT_NO_USECONTEXT } from './constants'; -import { checkHooks, IClientWithContext } from './utils'; +import { getControlTreatmentsWithConfig } from './constants'; +import { IClientWithContext } from './utils'; import { ISplitTreatmentsChildProps } from './types'; -import { INITIAL_CONTEXT } from './SplitContext'; import { useSplitClient } from './useSplitClient'; /** @@ -12,7 +11,7 @@ import { useSplitClient } from './useSplitClient'; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps { - const context = checkHooks(ERROR_UT_NO_USECONTEXT) ? useSplitClient(key) : INITIAL_CONTEXT; + const context = useSplitClient(key); const client = context.client; const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? client.getTreatmentsWithConfig(splitNames, attributes) : diff --git a/src/useTrack.ts b/src/useTrack.ts index adea31e..01fe8a0 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -1,6 +1,4 @@ import { useClient } from './useClient'; -import { ERROR_UTRACK_NO_USECONTEXT } from './constants'; -import { checkHooks } from './utils'; // no-op function that returns false const noOpFalse = () => false; @@ -13,6 +11,6 @@ const noOpFalse = () => false; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { - const client = checkHooks(ERROR_UTRACK_NO_USECONTEXT) ? useClient(key, trafficType) : null; + const client = useClient(key, trafficType); return client ? client.track.bind(client) : noOpFalse; } diff --git a/src/utils.ts b/src/utils.ts index 50f93f6..f5f44de 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -import React from 'react'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { VERSION } from './constants'; @@ -88,23 +87,6 @@ export function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus }; } -// Other utils - -/** - * Checks if React.useContext is available, and logs given message if not - * - * @param message - * @returns boolean indicating if React.useContext is available - */ -export function checkHooks(message: string): boolean { - if (!React.useContext) { - console.log(message); - return false; - } else { - return true; - } -} - // Input validation utils that will be replaced eventually export function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'split names'): false | string[] { diff --git a/types/utils.d.ts b/types/utils.d.ts index 71fc810..c12ec24 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -29,13 +29,6 @@ export interface IClientStatus { isDestroyed: boolean; } export declare function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus; -/** - * Checks if React.useContext is available, and logs given message if not - * - * @param message - * @returns boolean indicating if React.useContext is available - */ -export declare function checkHooks(message: string): boolean; export declare function validateFeatureFlags(maybeFeatureFlags: unknown, listName?: string): false | string[]; /** * Manage client attributes binding From 4ee1f42cc0d5707db3f87c5f08ac37980117d877 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 21 Jul 2023 22:32:23 -0300 Subject: [PATCH 17/67] removed unused string messages --- CHANGES.txt | 3 +++ src/__tests__/useClient.test.tsx | 11 ++++------- src/constants.ts | 10 ---------- types/constants.d.ts | 4 ---- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 03774ac..60848d1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +1.10.0 (XXX XX, 2023) + - Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error will be thrown rather than logging an error message. + 1.9.0 (July 18, 2023) - Updated some transitive dependencies for vulnerability fixes. - Updated @splitsoftware/splitio package to version 10.23.0 that includes: diff --git a/src/__tests__/useClient.test.tsx b/src/__tests__/useClient.test.tsx index e3a028c..5bcc4da 100644 --- a/src/__tests__/useClient.test.tsx +++ b/src/__tests__/useClient.test.tsx @@ -34,16 +34,13 @@ describe('useClient', () => { test('returns the client from the context updated by SplitClient.', () => { const outerFactory = SplitSdk(sdkBrowser); let client; - - const CustomComponent = () => { - client = useClient(); - return null; - } - render( - + {React.createElement(() => { + client = useClient(); + return null; + })} ); diff --git a/src/constants.ts b/src/constants.ts index 4f7be86..978dc58 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -38,14 +38,4 @@ export const ERROR_SC_NO_FACTORY: string = '[ERROR] SplitClient does not have ac export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; -const ERROR_NO_USECONTEXT = '[ERROR] Check your React version since `useContext` hook is not available. Split hooks require version 16.8.0+ of React.'; - -export const ERROR_UC_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning null from `useClient` hook.'; - -export const ERROR_UM_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning null from `useManager` hook.'; - -export const ERROR_UT_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning control treatments from `useTreatments` hook.'; - -export const ERROR_UTRACK_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning a no-op function from `useTrack` hook.'; - export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; diff --git a/types/constants.d.ts b/types/constants.d.ts index 2d52bf2..e382449 100644 --- a/types/constants.d.ts +++ b/types/constants.d.ts @@ -8,8 +8,4 @@ export declare const WARN_SF_CONFIG_AND_FACTORY: string; export declare const ERROR_SF_NO_CONFIG_AND_FACTORY: string; export declare const ERROR_SC_NO_FACTORY: string; export declare const WARN_ST_NO_CLIENT: string; -export declare const ERROR_UC_NO_USECONTEXT: string; -export declare const ERROR_UM_NO_USECONTEXT: string; -export declare const ERROR_UT_NO_USECONTEXT: string; -export declare const ERROR_UTRACK_NO_USECONTEXT: string; export declare const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string; From 3f1d6c7202e975c9a9c27cf4838af814e5c2b24c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 21 Jul 2023 23:35:32 -0300 Subject: [PATCH 18/67] simplify ternary expression --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 066c2d3..5bad37a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,7 +53,7 @@ export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.Split if (client.lastUpdate === undefined) { const updateLastUpdate = () => { const lastUpdate = Date.now(); - client.lastUpdate = lastUpdate > client.lastUpdate ? lastUpdate : lastUpdate === client.lastUpdate ? lastUpdate + 1 : client.lastUpdate + 1; + client.lastUpdate = lastUpdate > client.lastUpdate ? lastUpdate : client.lastUpdate + 1; } client.lastUpdate = 0; From 07ccecac00f6579ac34b32b4da00fb9eaa50f52d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 00:39:53 -0300 Subject: [PATCH 19/67] fixed useSplitClient to support changes on update options --- src/__tests__/useSplitClient.test.tsx | 39 +++++++++++++++++++++++++- src/useSplitClient.ts | 40 ++++++++++----------------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index cc8b87b..19ff057 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -15,7 +15,7 @@ import { useSplitClient } from '../useSplitClient'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; -test('useSplitClient', () => { +test('useSplitClient must update on SDK events', () => { const outerFactory = SplitSdk(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; @@ -128,3 +128,40 @@ test('useSplitClient', () => { expect(countNestedComponent).toEqual(4); }); + +test('useSplitClient must support changes in update props', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + + let rendersCount = 0; + + function InnerComponent(updateOptions) { + useSplitClient(undefined, undefined, undefined, updateOptions); + rendersCount++; + return null; + } + + function Component(updateOptions) { + return ( + + + + ) + } + + const wrapper = render(); + + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default + expect(rendersCount).toBe(2); + + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now + + expect(rendersCount).toBe(4); + + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now + + expect(rendersCount).toBe(5); +}); diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index dd23dbe..7ad1df2 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -17,8 +17,10 @@ const DEFAULT_OPTIONS = { * @return A Split Context object * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options: IUpdateProps = {}): ISplitContextValues { - options = { ...DEFAULT_OPTIONS, ...options }; +export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues { + const { + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate + } = { ...DEFAULT_OPTIONS, ...options }; const context = React.useContext(SplitContext); const { client: contextClient, factory } = context; @@ -35,37 +37,23 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att React.useEffect(() => { if (!client) return; - const setReady = () => { - if (options.updateOnSdkReady) setLastUpdate(client.lastUpdate); - } - - const setReadyFromCache = () => { - if (options.updateOnSdkReadyFromCache) setLastUpdate(client.lastUpdate); - } - - const setTimedout = () => { - if (options.updateOnSdkTimedout) setLastUpdate(client.lastUpdate); - } - - const setUpdate = () => { - if (options.updateOnSdkUpdate) setLastUpdate(client.lastUpdate); - } + const update = () => setLastUpdate(client.lastUpdate); // Subscribe to SDK events const status = getStatus(client); - if (!status.isReady) client.once(client.Event.SDK_READY, setReady); - if (!status.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache); - if (!status.hasTimedout && !status.isReady) client.once(client.Event.SDK_READY_TIMED_OUT, setTimedout); - client.on(client.Event.SDK_UPDATE, setUpdate); + if (!status.isReady && updateOnSdkReady) client.once(client.Event.SDK_READY, update); + if (!status.isReadyFromCache && updateOnSdkReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, update); + if (!status.hasTimedout && !status.isReady && updateOnSdkTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, update); + if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); return () => { // Unsubscribe from events - client.off(client.Event.SDK_READY, setReady); - client.off(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache); - client.off(client.Event.SDK_READY_TIMED_OUT, setTimedout); - client.off(client.Event.SDK_UPDATE, setUpdate); + client.off(client.Event.SDK_READY, update); + client.off(client.Event.SDK_READY_FROM_CACHE, update); + client.off(client.Event.SDK_READY_TIMED_OUT, update); + client.off(client.Event.SDK_UPDATE, update); } - }, [client]); + }, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); return { factory, client, ...getStatus(client) From 306b5223f1d81a1d324918ec00762e53deb38992 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 00:57:29 -0300 Subject: [PATCH 20/67] update comments --- src/__tests__/useSplitTreatments.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 5cf48ea..86bb084 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -75,7 +75,6 @@ test('useSplitTreatments', async () => { ); - // Awaiting to make sure each event is processed with a different lastUpdate timestamp. await act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); await act(() => mainClient.__emitter__.emit(Event.SDK_READY)); await act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); From 2fdf2f063df4825bcab4d36588670793e817122f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 01:50:46 -0300 Subject: [PATCH 21/67] added optimization tests --- src/__tests__/SplitTreatments.test.tsx | 48 +++++++++++++---------- src/__tests__/useSplitClient.test.tsx | 4 +- src/__tests__/useSplitTreatments.test.tsx | 23 ++++------- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index ce558e8..c37df09 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -25,6 +25,7 @@ jest.mock('../constants', () => { import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from '../constants'; import { getStatus } from '../utils'; import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; +import { useSplitTreatments } from '../useSplitTreatments'; describe('SplitTreatments', () => { @@ -143,13 +144,26 @@ describe('SplitTreatments', () => { }); +let renderTimes = 0; + /** - * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarely + * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments. */ -describe('SplitTreatments optimization', () => { - - let renderTimes = 0; - +describe.each([ + ({ names, attributes }) => ( + + {() => { + renderTimes++; + return null; + }} + + ), + ({ names, attributes }) => { + useSplitTreatments(names, attributes); + renderTimes++; + return null; + } +])('SplitTreatments & useSplitTreatments optimization', (InnerComponent) => { let outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); @@ -162,12 +176,7 @@ describe('SplitTreatments optimization', () => { return ( - - {() => { - renderTimes++; - return null; - }} - + ); @@ -224,7 +233,7 @@ describe('SplitTreatments optimization', () => { expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); }); - it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', (done) => { + it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', () => { expect(renderTimes).toBe(1); // State update and split evaluation @@ -234,16 +243,13 @@ describe('SplitTreatments optimization', () => { (outerFactory as any).client().destroy(); wrapper.rerender(); - setTimeout(() => { - // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching - expect(renderTimes).toBe(3); - expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching + expect(renderTimes).toBe(3); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); - // Restore the client to be READY - (outerFactory as any).client().__restore(); - (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - done(); - }) + // Restore the client to be READY + (outerFactory as any).client().__restore(); + (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); }); it('rerenders and re-evaluates feature flags if client changes.', () => { diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index d916a1b..19ff057 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -97,10 +97,10 @@ test('useSplitClient must update on SDK events', () => { ); act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => mainClient.__emitter__.emit(Event.SDK_READY)); - act(() => user2Client.__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)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 8ed4a9c..3c87720 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -34,14 +34,12 @@ function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTre } } -test('useSplitTreatments', async () => { +test('useSplitTreatments must update on SDK events', async () => { const outerFactory = SplitSdk(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; - const lastUpdateSetUser2 = new Set(); - const lastUpdateSetUser2WithUpdate = new Set(); render( @@ -63,7 +61,6 @@ test('useSplitTreatments', async () => { const context = useSplitTreatments(['split_test'], undefined, 'user_2'); expect(context.client).toBe(user2Client); validateTreatments(context); - lastUpdateSetUser2.add(context.lastUpdate); countUseSplitTreatmentsUser2++; return null; })} @@ -71,7 +68,6 @@ test('useSplitTreatments', async () => { const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true }); expect(context.client).toBe(user2Client); validateTreatments(context); - lastUpdateSetUser2WithUpdate.add(context.lastUpdate); countUseSplitTreatmentsUser2WithUpdate++; return null; })} @@ -79,15 +75,12 @@ test('useSplitTreatments', async () => { ); - // Adding a delay between events to make sure they are processed with a different lastUpdate timestamp. - act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - await new Promise(resolve => setTimeout(resolve, 10)); - act(() => mainClient.__emitter__.emit(Event.SDK_READY)); - act(() => user2Client.__emitter__.emit(Event.SDK_READY)); - await new Promise(resolve => setTimeout(resolve, 10)); - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); - act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); + await act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + await act(() => mainClient.__emitter__.emit(Event.SDK_READY)); + await act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); + await act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + await act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + await act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. expect(countSplitContext).toEqual(3); @@ -100,10 +93,8 @@ test('useSplitTreatments', async () => { // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); - expect(lastUpdateSetUser2.size).toEqual(3); // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); - expect(lastUpdateSetUser2WithUpdate.size).toEqual(4); expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); From f0ce66a41f7fd75472f0e2f752bf2d884c1cd1bd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 02:23:00 -0300 Subject: [PATCH 22/67] remove unnecessary delays and setTimeout in tests --- src/__tests__/SplitClient.test.tsx | 42 ++++++++------------------ src/__tests__/SplitTreatments.test.tsx | 21 +++++-------- 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 073a4ee..4bc4a90 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -253,36 +253,18 @@ describe('SplitClient', () => { this.state = { splitKey: 'user1' }; } - componentDidMount() { - setTimeout(() => { - act(() => this.setState({ splitKey: 'user2' })); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - setTimeout(() => { - act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - setTimeout(() => { - act(() => this.setState({ splitKey: 'user3' })); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); - setTimeout(() => { - act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY)); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); - setTimeout(() => { - act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE)); - setTimeout(() => { - expect(renderTimes).toBe(6); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); + async componentDidMount() { + await act(() => this.setState({ splitKey: 'user2' })); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + await act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + await act(() => this.setState({ splitKey: 'user3' })); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); + await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE)); + expect(renderTimes).toBe(6); + + done(); } render() { diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index ce558e8..7b5ed6a 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -224,7 +224,7 @@ describe('SplitTreatments optimization', () => { expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); }); - it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', (done) => { + it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', () => { expect(renderTimes).toBe(1); // State update and split evaluation @@ -234,16 +234,13 @@ describe('SplitTreatments optimization', () => { (outerFactory as any).client().destroy(); wrapper.rerender(); - setTimeout(() => { - // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching - expect(renderTimes).toBe(3); - expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching + expect(renderTimes).toBe(3); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); - // Restore the client to be READY - (outerFactory as any).client().__restore(); - (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - done(); - }) + // Restore the client to be READY + (outerFactory as any).client().__restore(); + (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); }); it('rerenders and re-evaluates feature flags if client changes.', () => { @@ -301,8 +298,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(2); expect(renderTimesComp2).toBe(2); // updateOnSdkReadyFromCache === false, in second component - // delay SDK events to guarantee a different lastUpdate timestamp for SplitTreatments to re-evaluate - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT); @@ -311,7 +306,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(3); expect(renderTimesComp2).toBe(3); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY); @@ -320,7 +314,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(3); // updateOnSdkReady === false, in first component expect(renderTimesComp2).toBe(4); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE); From 261c038b279fc8dbdb2ee4aa756fce2cd5870144 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 02:38:11 -0300 Subject: [PATCH 23/67] test polishing --- src/__tests__/SplitTreatments.test.tsx | 10 +++------- src/__tests__/useSplitClient.test.tsx | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index c37df09..e7145a4 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -252,9 +252,9 @@ describe.each([ (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); }); - it('rerenders and re-evaluates feature flags if client changes.', () => { + it('rerenders and re-evaluates feature flags if client changes.', async () => { wrapper.rerender(); - act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY)); // Initial render + 2 renders (in 3 updates) -> automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching expect(renderTimes).toBe(3); @@ -262,7 +262,7 @@ describe.each([ expect(outerFactory.client('otherKey').getTreatmentsWithConfig).toBeCalledTimes(1); }); - it('rerenders and re-evaluate splfeature flagsits when Split context changes (in both SplitFactory and SplitClient components).', async () => { + it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => { // changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments const outerFactory = SplitSdk(sdkBrowser); const names = ['split1', 'split2']; @@ -307,8 +307,6 @@ describe.each([ expect(renderTimesComp1).toBe(2); expect(renderTimesComp2).toBe(2); // updateOnSdkReadyFromCache === false, in second component - // delay SDK events to guarantee a different lastUpdate timestamp for SplitTreatments to re-evaluate - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT); @@ -317,7 +315,6 @@ describe.each([ expect(renderTimesComp1).toBe(3); expect(renderTimesComp2).toBe(3); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY); @@ -326,7 +323,6 @@ describe.each([ expect(renderTimesComp1).toBe(3); // updateOnSdkReady === false, in first component expect(renderTimesComp2).toBe(4); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE); diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 19ff057..4d6e489 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -68,9 +68,10 @@ test('useSplitClient must update on SDK events', () => { {React.createElement(() => { const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true }); - countNestedComponent++; - expect(status.client).toBe(user2Client); + + // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event + countNestedComponent++; switch (countNestedComponent) { case 1: expect(status.isReady).toBe(false); From 9dfa4bf97dfe13acbe036bff49c894a46a34e035 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 23 Jul 2023 01:20:43 -0300 Subject: [PATCH 24/67] add useTreatments test --- CHANGES.txt | 1 + src/__tests__/useTreatments.test.tsx | 34 ++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 60848d1..37891b8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 1.10.0 (XXX XX, 2023) + - 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). - Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error will be thrown rather than logging an error message. 1.9.0 (July 18, 2023) diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx index 1671948..2116f92 100644 --- a/src/__tests__/useTreatments.test.tsx +++ b/src/__tests__/useTreatments.test.tsx @@ -80,26 +80,42 @@ describe('useTreatments', () => { expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); }); - test('returns the Treatments from a new client given a splitKey, or control if the client is not operational.', async () => { + test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client('user2'); - let treatments; - - client.__emitter__.emit(Event.SDK_READY); - await client.destroy(); + let renderTimes = 0; render( {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes, 'user2'); + const treatments = useTreatments(featureFlagNames, attributes, 'user2'); + + renderTimes++; + switch (renderTimes) { + case 1: + // returns control if not operational (SDK not ready), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + break; + case 2: + case 3: + // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags + expect(client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveLastReturnedWith(treatments); + break; + default: + throw new Error('Unexpected render'); + } + return null; })} ); - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => client.__emitter__.emit(Event.SDK_READY)); + act(() => client.__emitter__.emit(Event.SDK_UPDATE)); // should not trigger a re-render by default + expect(client.getTreatmentsWithConfig).toBeCalledTimes(2); }); // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' From 38d511984ba902a78811b57b0c91c2f9577ed3c9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 23 Jul 2023 01:47:15 -0300 Subject: [PATCH 25/67] changelog --- CHANGES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 60848d1..141dbcf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,6 @@ 1.10.0 (XXX XX, 2023) + - Added a new parameter `options` to `useClient` and `useTreatments` hooks, to allow controlling on what SDK events the hook should update the component. Read more in our docs. + - Added new `useSplitClient` and `useSplitTreatments` hooks. Their input parameters and logic are equivalent to `useClient` and `useTreatments` hooks, but rather than returning only the SDK client and treatments respectively, they return the Split context object together with the SDK client and treatments. Read more in our docs. - Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error will be thrown rather than logging an error message. 1.9.0 (July 18, 2023) From 567ed42e394cb46ebf65a2853730381851376dd1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 23 Jul 2023 13:12:46 -0300 Subject: [PATCH 26/67] update test --- src/__tests__/useSplitClient.test.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 19ff057..13261c8 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -30,19 +30,25 @@ test('useSplitClient must update on SDK events', () => { {() => countSplitContext++} - + {() => { countSplitClient++; return null }} - - {() => { countSplitClientUser2++; return null }} - {React.createElement(() => { - const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' }); + // Equivalent to + // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` + // - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` + const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }); expect(client).toBe(mainClient); // Assert that the main client was retrieved. expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. countUseSplitClient++; return null; })} + + {() => { countSplitClientUser2++; return null }} + {React.createElement(() => { const { client } = useSplitClient('user_2'); expect(client).toBe(user2Client); From 6b37b376190c24ffd5bceb5f11e62e37b43120e4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Aug 2023 16:12:06 -0300 Subject: [PATCH 27/67] useTrack update --- src/useTrack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useTrack.ts b/src/useTrack.ts index 01fe8a0..9808375 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -11,6 +11,6 @@ const noOpFalse = () => false; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { - const client = useClient(key, trafficType); + const client = useClient(key, trafficType, undefined, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); return client ? client.track.bind(client) : noOpFalse; } From 7d0f0a84b66cc2717dbdb2a7c0edc8a200daac1d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sat, 22 Jul 2023 23:38:32 -0300 Subject: [PATCH 28/67] polishing --- CHANGES.txt | 2 +- src/SplitClient.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 60848d1..2e35fd8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ 1.10.0 (XXX XX, 2023) - - Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error will be thrown rather than logging an error message. + - Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error is thrown instead of logging an error message. 1.9.0 (July 18, 2023) - Updated some transitive dependencies for vulnerability fixes. diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 7c53172..9478235 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -22,7 +22,7 @@ export class SplitComponent extends React.Component Date: Wed, 13 Sep 2023 12:25:24 -0300 Subject: [PATCH 29/67] reuse DEFAULT_UPDATE_OPTIONS const --- src/SplitClient.tsx | 6 ++---- src/SplitFactory.tsx | 6 ++---- src/useSplitClient.ts | 10 +++++----- types/useSplitClient.d.ts | 6 ++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index a78be93..9e8f88a 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -3,6 +3,7 @@ import { SplitContext } from './SplitContext'; import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; import { ERROR_SC_NO_FACTORY } from './constants'; import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils'; +import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; /** * Common component used to handle the status and events of a Split client passed as prop. @@ -11,13 +12,10 @@ import { getStatus, getSplitClient, initAttributes, IClientWithContext } from '. export class SplitComponent extends React.Component { static defaultProps = { - updateOnSdkUpdate: false, - updateOnSdkTimedout: false, - updateOnSdkReady: true, - updateOnSdkReadyFromCache: true, children: null, factory: null, client: null, + ...DEFAULT_UPDATE_OPTIONS, } // Using `getDerivedStateFromProps` since the state depends on the status of the client in props, which might change over time. diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx index 5460e0e..ba2c12d 100644 --- a/src/SplitFactory.tsx +++ b/src/SplitFactory.tsx @@ -4,6 +4,7 @@ import { SplitComponent } from './SplitClient'; import { ISplitFactoryProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants'; import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils'; +import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; /** * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, @@ -18,11 +19,8 @@ import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClie export class SplitFactory extends React.Component { static defaultProps: ISplitFactoryProps = { - updateOnSdkUpdate: false, - updateOnSdkTimedout: false, - updateOnSdkReady: true, - updateOnSdkReadyFromCache: true, children: null, + ...DEFAULT_UPDATE_OPTIONS, }; readonly state: Readonly<{ factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }>; diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index ff116d0..2294813 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -3,7 +3,7 @@ import { SplitContext } from './SplitContext'; import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils'; import { ISplitContextValues } from './types'; -const options = { +export const DEFAULT_UPDATE_OPTIONS = { updateOnSdkUpdate: false, updateOnSdkTimedout: false, updateOnSdkReady: true, @@ -34,19 +34,19 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att if (!client) return; const setReady = () => { - if (options.updateOnSdkReady) setLastUpdate(client.lastUpdate); + if (DEFAULT_UPDATE_OPTIONS.updateOnSdkReady) setLastUpdate(client.lastUpdate); } const setReadyFromCache = () => { - if (options.updateOnSdkReadyFromCache) setLastUpdate(client.lastUpdate); + if (DEFAULT_UPDATE_OPTIONS.updateOnSdkReadyFromCache) setLastUpdate(client.lastUpdate); } const setTimedout = () => { - if (options.updateOnSdkTimedout) setLastUpdate(client.lastUpdate); + if (DEFAULT_UPDATE_OPTIONS.updateOnSdkTimedout) setLastUpdate(client.lastUpdate); } const setUpdate = () => { - if (options.updateOnSdkUpdate) setLastUpdate(client.lastUpdate); + if (DEFAULT_UPDATE_OPTIONS.updateOnSdkUpdate) setLastUpdate(client.lastUpdate); } // Subscribe to SDK events diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts index 64297af..093336b 100644 --- a/types/useSplitClient.d.ts +++ b/types/useSplitClient.d.ts @@ -1,4 +1,10 @@ import { ISplitContextValues } from './types'; +export declare const DEFAULT_UPDATE_OPTIONS: { + updateOnSdkUpdate: boolean; + updateOnSdkTimedout: boolean; + updateOnSdkReady: boolean; + updateOnSdkReadyFromCache: boolean; +}; /** * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. From 01ef2db1227d85afd8754a51e9ff6621854411df Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Sep 2023 15:45:55 -0300 Subject: [PATCH 30/67] export TypeScript types and interfaces from the library index --- CHANGES.txt | 3 ++- src/__tests__/index.test.ts | 11 +++++++++++ src/index.ts | 13 +++++++++++++ types/index.d.ts | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6edb4be..ebc5e62 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ -1.9.1 (September XX, 2023) +1.10.0 (September XX, 2023) + - 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 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). diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 647dbd8..9020eff 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { SplitContext as ExportedSplitContext, SplitSdk as ExportedSplitSdk, @@ -11,6 +12,16 @@ import { useManager as exportedUseManager, useTrack as exportedUseTrack, useTreatments as exportedUseTreatments, + // Checks that types are exported. Otherwise, the test would fail with a TS error. + ISplitClientChildProps, + ISplitClientProps, + ISplitContextValues, + ISplitFactoryChildProps, + ISplitFactoryProps, + ISplitStatus, + ISplitTreatmentsChildProps, + ISplitTreatmentsProps, + IUpdateProps } from '../index'; import { SplitContext } from '../SplitContext'; import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client'; diff --git a/src/index.ts b/src/index.ts index 52a0de8..00f21a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,3 +19,16 @@ export { useManager } from './useManager'; // SplitContext export { SplitContext } from './SplitContext'; + +// Types +export type { + ISplitClientChildProps, + ISplitClientProps, + ISplitContextValues, + ISplitFactoryChildProps, + ISplitFactoryProps, + ISplitStatus, + ISplitTreatmentsChildProps, + ISplitTreatmentsProps, + IUpdateProps +} from './types'; diff --git a/types/index.d.ts b/types/index.d.ts index 91b8d2a..f46a4e8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -10,3 +10,4 @@ export { useTreatments } from './useTreatments'; export { useTrack } from './useTrack'; export { useManager } from './useManager'; export { SplitContext } from './SplitContext'; +export type { ISplitClientChildProps, ISplitClientProps, ISplitContextValues, ISplitFactoryChildProps, ISplitFactoryProps, ISplitStatus, ISplitTreatmentsChildProps, ISplitTreatmentsProps, IUpdateProps } from './types'; From f1cc018f2221ffc563232d70e8976d94e8beb69f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Sep 2023 11:14:00 -0300 Subject: [PATCH 31/67] Remove unnecessary 'async' keyword from tests --- src/__tests__/useSplitTreatments.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 86bb084..de8e120 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -75,12 +75,12 @@ test('useSplitTreatments', async () => { ); - await act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - await act(() => mainClient.__emitter__.emit(Event.SDK_READY)); - await act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); - await act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - await act(() => user2Client.__emitter__.emit(Event.SDK_READY)); - await act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); + 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)); // SplitContext renders 3 times: initially, when ready from cache, and when ready. expect(countSplitContext).toEqual(3); From 70c4a97fcce1996fc0e98fa80b374fa87404076a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Sep 2023 11:31:27 -0300 Subject: [PATCH 32/67] Add changelog entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index ebc5e62..9ec5191 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 1.10.0 (September XX, 2023) - 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 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 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). From 7828904500153822a0201e78d26ae1c36e28792e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Sep 2023 11:37:41 -0300 Subject: [PATCH 33/67] prepare rc --- .github/workflows/ci.yml | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3a8b1..5e82593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/hooks_export') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/development' + if: github.event_name == 'push' && github.ref == 'refs/heads/hooks_export' strategy: matrix: environment: diff --git a/package-lock.json b/package-lock.json index 9f38433..d1ef920 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.0", + "version": "1.9.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.0", + "version": "1.9.1-rc.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.23.0", diff --git a/package.json b/package.json index 206ca9b..64d5d20 100644 --- a/package.json +++ b/package.json @@ -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", From a85623b06d02f803e002877e980d6c89680ea2c4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Sep 2023 12:59:42 -0300 Subject: [PATCH 34/67] Fix typo --- .github/workflows/ci.yml | 4 ++-- src/types.ts | 4 ++-- types/types.d.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e82593..bc3a8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/hooks_export') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/hooks_export' + if: github.event_name == 'push' && github.ref == 'refs/heads/development' strategy: matrix: environment: diff --git a/src/types.ts b/src/types.ts index 5606976..da8f376 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,7 +101,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { } /** * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. + * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { @@ -136,7 +136,7 @@ export interface ISplitClientChildProps extends ISplitContextValues { } /** * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. + * used to instantiate a new client instance, update the Split context, and listen for SDK events. */ export interface ISplitClientProps extends IUpdateProps { diff --git a/types/types.d.ts b/types/types.d.ts index 82ffa01..6734c0e 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -84,7 +84,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { } /** * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. + * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { /** @@ -112,7 +112,7 @@ export interface ISplitClientChildProps extends ISplitContextValues { } /** * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. + * used to instantiate a new client instance, update the Split context, and listen for SDK events. */ export interface ISplitClientProps extends IUpdateProps { /** From 6d8bd73b1876a78708b2289244e1d70d090245b2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 5 Oct 2023 16:39:38 -0300 Subject: [PATCH 35/67] fix typo --- src/__tests__/useTrack.test.tsx | 24 ++++++++++++------------ src/useTrack.ts | 2 +- types/useTrack.d.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/__tests__/useTrack.test.tsx b/src/__tests__/useTrack.test.tsx index 317af05..11ecce4 100644 --- a/src/__tests__/useTrack.test.tsx +++ b/src/__tests__/useTrack.test.tsx @@ -21,16 +21,16 @@ describe('useTrack', () => { const value = 10; const properties = { prop1: 'prop1' }; - test('returns the track method binded to the client at Split context updated by SplitFactory.', () => { + test('returns the track method bound to the client at Split context updated by SplitFactory.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack(); - trackResult = bindedTrack(tt, eventType, value, properties); + boundTrack = useTrack(); + trackResult = boundTrack(tt, eventType, value, properties); return null; })} , @@ -40,17 +40,17 @@ describe('useTrack', () => { expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method binded to the client at Split context updated by SplitClient.', () => { + test('returns the track method bound to the client at Split context updated by SplitClient.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack(); - trackResult = bindedTrack(tt, eventType, value, properties); + boundTrack = useTrack(); + trackResult = boundTrack(tt, eventType, value, properties); return null; })} @@ -61,16 +61,16 @@ describe('useTrack', () => { expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method binded to a new client given a splitKey and optional trafficType.', () => { + test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack('user2', tt); - trackResult = bindedTrack(eventType, value, properties); + boundTrack = useTrack('user2', tt); + trackResult = boundTrack(eventType, value, properties); return null; })} , diff --git a/src/useTrack.ts b/src/useTrack.ts index 01fe8a0..95b13df 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -7,7 +7,7 @@ const noOpFalse = () => false; * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false. + * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { diff --git a/types/useTrack.d.ts b/types/useTrack.d.ts index 6479f79..e983136 100644 --- a/types/useTrack.d.ts +++ b/types/useTrack.d.ts @@ -2,7 +2,7 @@ * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false. + * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export declare function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track']; From 5c5274b240983de0363080370f86765906cd6a30 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 5 Oct 2023 16:43:08 -0300 Subject: [PATCH 36/67] add comment about useTrack implementation --- src/useTrack.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/useTrack.ts b/src/useTrack.ts index 9808375..103071f 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -11,6 +11,7 @@ const noOpFalse = () => false; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { + // All update options are false to avoid re-renders. The track method doesn't need the client to be operational. const client = useClient(key, trafficType, undefined, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); return client ? client.track.bind(client) : noOpFalse; } From d15e1b3acdd30eea720fc58fad95c14a16bff5aa Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 14:05:48 -0300 Subject: [PATCH 37/67] implementation of new hooks with options object --- package-lock.json | 12 +++---- src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/useSplitClient.test.tsx | 12 +++---- src/__tests__/useSplitTreatments.test.tsx | 6 ++-- src/types.ts | 43 ++++++++++++++++------- src/useClient.ts | 4 +-- src/useSplitClient.ts | 10 +++--- src/useSplitTreatments.ts | 13 +++---- src/useTrack.ts | 7 ++-- src/useTreatments.ts | 4 +-- types/types.d.ts | 36 +++++++++++++------ types/useClient.d.ts | 2 +- types/useSplitClient.d.ts | 4 +-- types/useSplitTreatments.d.ts | 4 +-- types/useTrack.d.ts | 2 +- types/useTreatments.d.ts | 2 +- 16 files changed, 100 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb6d8e7..5f01255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10149,9 +10149,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.3.2", @@ -18581,9 +18581,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "type-check": { "version": "0.3.2", diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index e7145a4..f3c2f56 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -159,7 +159,7 @@ describe.each([ ), ({ names, attributes }) => { - useSplitTreatments(names, attributes); + useSplitTreatments({ names, attributes }); renderTimes++; return null; } diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 543032d..8551c7b 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -40,7 +40,7 @@ test('useSplitClient must update on SDK events', () => { // Equivalent to // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` // - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` - const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }); + const { client } = useSplitClient({ attributes: { att1: 'att1' } }); expect(client).toBe(mainClient); // Assert that the main client was retrieved. expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. countUseSplitClient++; @@ -50,7 +50,7 @@ test('useSplitClient must update on SDK events', () => { {() => { countSplitClientUser2++; return null }} {React.createElement(() => { - const { client } = useSplitClient('user_2'); + const { client } = useSplitClient({ splitKey: 'user_2' }); expect(client).toBe(user2Client); countUseSplitClientUser2++; return null; @@ -59,7 +59,7 @@ test('useSplitClient must update on SDK events', () => { {() => { countSplitClientWithUpdate++; return null }} {React.createElement(() => { - useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, undefined, { updateOnSdkUpdate: true }).client; + useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; countUseSplitClientWithUpdate++; return null; })} @@ -67,13 +67,13 @@ test('useSplitClient must update on SDK events', () => { {() => { countSplitClientUser2WithUpdate++; return null }} {React.createElement(() => { - useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true }); + useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); countUseSplitClientUser2WithUpdate++; return null; })} {React.createElement(() => { - const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true }); + const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); expect(status.client).toBe(user2Client); // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event @@ -143,7 +143,7 @@ test('useSplitClient must support changes in update props', () => { let rendersCount = 0; function InnerComponent(updateOptions) { - useSplitClient(undefined, undefined, undefined, updateOptions); + useSplitClient(updateOptions); rendersCount++; return null; } diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index b89fbee..5fe57a9 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -51,21 +51,21 @@ test('useSplitTreatments must update on SDK events', async () => { {() => { countSplitTreatments++; return null }} {React.createElement(() => { - const context = useSplitTreatments(['split_test'], { att1: 'att1' }); + const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. validateTreatments(context); countUseSplitTreatments++; return null; })} {React.createElement(() => { - const context = useSplitTreatments(['split_test'], undefined, 'user_2'); + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); expect(context.client).toBe(user2Client); validateTreatments(context); countUseSplitTreatmentsUser2++; return null; })} {React.createElement(() => { - const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true }); + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true }); expect(context.client).toBe(user2Client); validateTreatments(context); countUseSplitTreatmentsUser2WithUpdate++; diff --git a/src/types.ts b/src/types.ts index 8349c49..42d2a85 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,7 +102,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { } /** * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. + * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { @@ -129,22 +129,15 @@ export interface ISplitFactoryProps extends IUpdateProps { } /** - * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. - */ -// @TODO remove next type (breaking-change) -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ISplitClientChildProps extends ISplitContextValues { } - -/** - * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. + * useSplitClient options interface. This is the options object accepted by useSplitClient hook, + * used to retrieve a client instance with the Split context, and listen for SDK events. */ -export interface ISplitClientProps extends IUpdateProps { +export interface IUseSplitClientOptions extends IUpdateProps { /** * The customer identifier. */ - splitKey: SplitIO.SplitKey; + splitKey?: SplitIO.SplitKey; /** * Traffic type associated with the customer identifier. @@ -156,6 +149,20 @@ export interface ISplitClientProps extends IUpdateProps { * An object of type Attributes used to evaluate the feature flags. */ attributes?: SplitIO.Attributes; +} + +/** + * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. + */ +// @TODO remove next type (breaking-change) +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ISplitClientChildProps extends ISplitContextValues { } + +/** + * SplitClient Props interface. These are the props accepted by SplitClient component, + * used to instantiate a new client instance, update the Split context, and listen for SDK events. + */ +export interface ISplitClientProps extends IUseSplitClientOptions { /** * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. @@ -163,6 +170,18 @@ export interface ISplitClientProps extends IUpdateProps { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } +/** + * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, + * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + */ +export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { + + /** + * list of feature flag names + */ + names: string[] +} + /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ diff --git a/src/useClient.ts b/src/useClient.ts index ddafee0..437fc8a 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -8,6 +8,6 @@ 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} */ -export function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { - return useSplitClient(key, trafficType, attributes).client; +export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { + return useSplitClient({ splitKey, trafficType, attributes }).client; } diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index d5aa594..cd3a11c 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,7 +1,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils'; -import { ISplitContextValues, IUpdateProps } from './types'; +import { ISplitContextValues, IUseSplitClientOptions } from './types'; export const DEFAULT_UPDATE_OPTIONS = { updateOnSdkUpdate: false, @@ -17,17 +17,17 @@ export const DEFAULT_UPDATE_OPTIONS = { * @return A Split Context object * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues { +export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues { const { - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, trafficType, attributes } = { ...DEFAULT_UPDATE_OPTIONS, ...options }; const context = React.useContext(SplitContext); const { client: contextClient, factory } = context; let client = contextClient as IClientWithContext; - if (key && factory) { - client = getSplitClient(factory, key, trafficType); + if (splitKey && factory) { + client = getSplitClient(factory, splitKey, trafficType); } initAttributes(client, attributes); diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index ab4d196..ddc0590 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,7 +1,7 @@ import React from 'react'; import { getControlTreatmentsWithConfig } from './constants'; import { IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils'; -import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; +import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; /** @@ -11,15 +11,16 @@ import { useSplitClient } from './useSplitClient'; * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ -export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps { - const context = useSplitClient(key, undefined, undefined, options); - const client = context.client; +export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { + const context = useSplitClient({...options, attributes: undefined }); + const { client, lastUpdate } = context; + const { names, attributes } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - getTreatmentsWithConfig(client, context.lastUpdate, splitNames, attributes, { ...client.getAttributes() }) : - getControlTreatmentsWithConfig(splitNames); + getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }) : + getControlTreatmentsWithConfig(names); return { ...context, diff --git a/src/useTrack.ts b/src/useTrack.ts index 95b13df..108885e 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -1,4 +1,4 @@ -import { useClient } from './useClient'; +import { useSplitClient } from './useSplitClient'; // no-op function that returns false const noOpFalse = () => false; @@ -10,7 +10,8 @@ const noOpFalse = () => false; * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ -export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { - const client = useClient(key, trafficType); +export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { + // All update options are false to avoid re-renders. The track method doesn't need the client to be operational. + const { client } = useSplitClient({ splitKey, trafficType, updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); return client ? client.track.bind(client) : noOpFalse; } diff --git a/src/useTreatments.ts b/src/useTreatments.ts index 4aed7d8..3a152a5 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -8,6 +8,6 @@ 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} */ -export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { - return useSplitTreatments(featureFlagNames, attributes, key).treatments; +export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { + return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments; } diff --git a/types/types.d.ts b/types/types.d.ts index 7a35cdc..0e4c275 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -84,7 +84,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { } /** * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. + * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { /** @@ -106,19 +106,14 @@ export interface ISplitFactoryProps extends IUpdateProps { children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode; } /** - * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. - */ -export interface ISplitClientChildProps extends ISplitContextValues { -} -/** - * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. + * useSplitClient options interface. This is the options object accepted by useSplitClient hook, + * used to retrieve a client instance with the Split context, and listen for SDK events. */ -export interface ISplitClientProps extends IUpdateProps { +export interface IUseSplitClientOptions extends IUpdateProps { /** * The customer identifier. */ - splitKey: SplitIO.SplitKey; + splitKey?: SplitIO.SplitKey; /** * Traffic type associated with the customer identifier. * If no provided here or at the config object, it will be required on the client.track() calls. @@ -128,11 +123,32 @@ export interface ISplitClientProps extends IUpdateProps { * An object of type Attributes used to evaluate the feature flags. */ attributes?: SplitIO.Attributes; +} +/** + * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. + */ +export interface ISplitClientChildProps extends ISplitContextValues { +} +/** + * SplitClient Props interface. These are the props accepted by SplitClient component, + * used to instantiate a new client instance, update the Split context, and listen for SDK events. + */ +export interface ISplitClientProps extends IUseSplitClientOptions { /** * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. */ children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } +/** + * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, + * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + */ +export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { + /** + * list of feature flag names + */ + names: string[]; +} /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ diff --git a/types/useClient.d.ts b/types/useClient.d.ts index 75284f7..d08dbf4 100644 --- a/types/useClient.d.ts +++ b/types/useClient.d.ts @@ -6,4 +6,4 @@ * @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} */ -export declare function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; +export declare function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts index 761f787..90868e4 100644 --- a/types/useSplitClient.d.ts +++ b/types/useSplitClient.d.ts @@ -1,4 +1,4 @@ -import { ISplitContextValues, IUpdateProps } from './types'; +import { ISplitContextValues, IUseSplitClientOptions } from './types'; export declare const DEFAULT_UPDATE_OPTIONS: { updateOnSdkUpdate: boolean; updateOnSdkTimedout: boolean; @@ -12,4 +12,4 @@ export declare const DEFAULT_UPDATE_OPTIONS: { * @return A Split Context object * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues; +export declare function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues; diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts index edbda7f..b636632 100644 --- a/types/useSplitTreatments.d.ts +++ b/types/useSplitTreatments.d.ts @@ -1,4 +1,4 @@ -import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; +import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; /** * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. @@ -6,4 +6,4 @@ import { ISplitTreatmentsChildProps, IUpdateProps } from './types'; * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ -export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps; +export declare function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps; diff --git a/types/useTrack.d.ts b/types/useTrack.d.ts index e983136..ff25094 100644 --- a/types/useTrack.d.ts +++ b/types/useTrack.d.ts @@ -5,4 +5,4 @@ * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ -export declare function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track']; +export declare function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track']; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts index 6b688e7..fd8170e 100644 --- a/types/useTreatments.d.ts +++ b/types/useTreatments.d.ts @@ -6,4 +6,4 @@ * @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} */ -export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; +export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; From 064a8729f354d57287ee7eac03fdc97504e62b7a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 14:39:28 -0300 Subject: [PATCH 38/67] update CHANGES log entry --- CHANGES.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5f93890..6236b6a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,12 @@ -1.10.0 (September XX, 2023) - - Added a new parameter `options` to `useClient` and `useTreatments` hooks, to allow controlling on what SDK events the hook should update the component. Read more in our docs. - - Added new `useSplitClient` and `useSplitTreatments` hooks. Their input parameters and logic are equivalent to `useClient` and `useTreatments` hooks, but rather than returning only the SDK client and treatments respectively, they return the Split context object together with the SDK client and treatments. Read more in our docs. +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). From 3af2878db50eacc448f3ac359442c9d101bcc9c4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 14:47:42 -0300 Subject: [PATCH 39/67] update README and do some polishing --- README.md | 38 ++++++++++++++++++++------------------ src/useClient.ts | 2 ++ src/useTreatments.ts | 2 ++ src/utils.ts | 2 +- types/useClient.d.ts | 2 ++ types/useTreatments.d.ts | 2 ++ 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 18ce0c7..f936823 100644 --- a/README.md +++ b/README.md @@ -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 = { @@ -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
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 + } 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 - {/* Evaluate feature flags with SplitTreatments component */} - - {({ treatments: { FEATURE_FLAG_NAME }, isReady }) => { - // 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 - } else { - // return JSX for control treatment - } - }} -
+
); } diff --git a/src/useClient.ts b/src/useClient.ts index 437fc8a..517dc81 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -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 useSplitClient is the new hook to use. */ export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { return useSplitClient({ splitKey, trafficType, attributes }).client; diff --git a/src/useTreatments.ts b/src/useTreatments.ts index 3a152a5..e6ba0e3 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -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 useSplitTreatments is the new hook to use. */ export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments; diff --git a/src/utils.ts b/src/utils.ts index d3ca36a..5d48611 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,7 +49,7 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC // idempotent operation export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation - const client = (key ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + const client = (key !== undefined ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; // Handle client lastUpdate if (client.lastUpdate === undefined) { diff --git a/types/useClient.d.ts b/types/useClient.d.ts index d08dbf4..85f91c9 100644 --- a/types/useClient.d.ts +++ b/types/useClient.d.ts @@ -5,5 +5,7 @@ * * @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 useSplitClient is the new hook to use. */ export declare function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts index fd8170e..d48695b 100644 --- a/types/useTreatments.d.ts +++ b/types/useTreatments.d.ts @@ -5,5 +5,7 @@ * * @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 useSplitTreatments is the new hook to use. */ export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; From a30854ba49b216498a4571e1914588ef72992b2c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 14:57:34 -0300 Subject: [PATCH 40/67] add useSplitManager --- CHANGES.txt | 8 ++++---- src/__tests__/index.test.ts | 3 +++ src/index.ts | 3 ++- src/useManager.ts | 8 ++++---- src/useSplitManager.ts | 19 +++++++++++++++++++ types/index.d.ts | 1 + types/useManager.d.ts | 2 ++ types/useSplitManager.d.ts | 11 +++++++++++ 8 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 src/useSplitManager.ts create mode 100644 types/useSplitManager.d.ts diff --git a/CHANGES.txt b/CHANGES.txt index 6236b6a..905d4e0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,9 @@ 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`. + - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks to use instead of `useClient`, `useTreatments` and `useManager` respectively, which are deprecated now. + - The new hooks return the Split context object together with the SDK client, treatments and manager respectively, rather than returning only the client, treatments or manager 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. + - `useClient` and `useTreatments` 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). + - They fixed a bug in the deprecated `useClient` and `useTreatments` 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 evaluations. diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 3cc9a1b..3604c01 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -14,6 +14,7 @@ import { useTreatments as exportedUseTreatments, useSplitClient as exportedUseSplitClient, useSplitTreatments as exportedUseSplitTreatments, + useSplitManager as exportedUseSplitManager, // Checks that types are exported. Otherwise, the test would fail with a TS error. ISplitClientChildProps, ISplitClientProps, @@ -39,6 +40,7 @@ import { useTrack } from '../useTrack'; import { useTreatments } from '../useTreatments'; import { useSplitClient } from '../useSplitClient'; import { useSplitTreatments } from '../useSplitTreatments'; +import { useSplitManager } from '../useSplitManager'; describe('index', () => { @@ -61,6 +63,7 @@ describe('index', () => { expect(exportedUseTreatments).toBe(useTreatments); expect(exportedUseSplitClient).toBe(useSplitClient); expect(exportedUseSplitTreatments).toBe(useSplitTreatments); + expect(exportedUseSplitManager).toBe(useSplitManager); }); it('should export SplitContext', () => { diff --git a/src/index.ts b/src/index.ts index ce3726c..6aff505 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,13 +11,14 @@ export { SplitTreatments } from './SplitTreatments'; export { SplitClient } from './SplitClient'; export { SplitFactory } from './SplitFactory'; -// helper functions/hooks +// Hooks export { useClient } from './useClient'; export { useTreatments } from './useTreatments'; export { useTrack } from './useTrack'; export { useManager } from './useManager'; export { useSplitClient } from './useSplitClient'; export { useSplitTreatments } from './useSplitTreatments'; +export { useSplitManager } from './useSplitManager'; // SplitContext export { SplitContext } from './SplitContext'; diff --git a/src/useManager.ts b/src/useManager.ts index beceb38..a0383f3 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -1,5 +1,4 @@ -import React from 'react'; -import { SplitContext } from './SplitContext'; +import { useSplitManager } from './useSplitManager'; /** * 'useManager' is a hook that returns the Manager instance from the Split factory. @@ -8,8 +7,9 @@ import { SplitContext } from './SplitContext'; * * @return A Split Manager instance, or null if used outside the scope of SplitFactory * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + * + * @deprecated useSplitManager is the new hook to use. */ export function useManager(): SplitIO.IManager | null { - const { factory } = React.useContext(SplitContext); - return factory ? factory.manager() : null; + return useSplitManager().manager; } diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts new file mode 100644 index 0000000..df54a50 --- /dev/null +++ b/src/useSplitManager.ts @@ -0,0 +1,19 @@ +import React from 'react'; +import { SplitContext } from './SplitContext'; +import { ISplitContextValues } from './types'; + +/** + * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. + * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. + * + * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + */ +export function useSplitManager(): ISplitContextValues & { manager: SplitIO.IManager | null } { + // Update options are not supported, because updates can be controlled at the SplitFactory component. + const context = React.useContext(SplitContext); + return { + ...context, + manager: context.factory ? context.factory.manager() : null + }; +} diff --git a/types/index.d.ts b/types/index.d.ts index 76e03f3..d9499c0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -11,5 +11,6 @@ export { useTrack } from './useTrack'; export { useManager } from './useManager'; export { useSplitClient } from './useSplitClient'; export { useSplitTreatments } from './useSplitTreatments'; +export { useSplitManager } from './useSplitManager'; export { SplitContext } from './SplitContext'; export type { ISplitClientChildProps, ISplitClientProps, ISplitContextValues, ISplitFactoryChildProps, ISplitFactoryProps, ISplitStatus, ISplitTreatmentsChildProps, ISplitTreatmentsProps, IUpdateProps } from './types'; diff --git a/types/useManager.d.ts b/types/useManager.d.ts index 68257aa..33ffbf2 100644 --- a/types/useManager.d.ts +++ b/types/useManager.d.ts @@ -5,5 +5,7 @@ * * @return A Split Manager instance, or null if used outside the scope of SplitFactory * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + * + * @deprecated useSplitManager is the new hook to use. */ export declare function useManager(): SplitIO.IManager | null; diff --git a/types/useSplitManager.d.ts b/types/useSplitManager.d.ts new file mode 100644 index 0000000..a1c3d3e --- /dev/null +++ b/types/useSplitManager.d.ts @@ -0,0 +1,11 @@ +import { ISplitContextValues } from './types'; +/** + * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. + * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. + * + * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + */ +export declare function useSplitManager(): ISplitContextValues & { + manager: SplitIO.IManager | null; +}; From 7318154d86b8292283a3e502f6cb5a452f22e525 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 15:12:17 -0300 Subject: [PATCH 41/67] add useSplitManager test --- src/__tests__/useSplitManager.test.tsx | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/__tests__/useSplitManager.test.tsx diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx new file mode 100644 index 0000000..807f068 --- /dev/null +++ b/src/__tests__/useSplitManager.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** Mocks */ +import { mockSdk } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; + +/** Test target */ +import { SplitFactory } from '../SplitFactory'; +import { useSplitManager } from '../useSplitManager'; + +describe('useSplitManager', () => { + + test('returns the factory manager from the Split context.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let hookResult; + render( + + {React.createElement(() => { + hookResult = useSplitManager(); + return null; + })} + + ); + expect(hookResult).toStrictEqual({ + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, + hasTimedout: false, + isDestroyed: false, + isReady: false, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: 0, + }); + }); + + test('returns null if invoked outside Split context.', () => { + let hookResult; + render( + React.createElement(() => { + hookResult = useSplitManager(); + return null; + }) + ); + expect(hookResult).toStrictEqual({ + manager: null, + client: null, + factory: null, + hasTimedout: false, + isDestroyed: false, + isReady: false, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: 0, + }); + }); + +}); From ec5a663d849e1e7f342172ce04e236096b1ebd31 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 17:59:23 -0300 Subject: [PATCH 42/67] add flagSets prop to SplitTreatments component and flagSets property to useSplitTreatments hook --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- src/SplitTreatments.tsx | 9 +++++---- src/types.ts | 16 +++++++++++++--- src/useSplitTreatments.ts | 9 +++++---- src/utils.ts | 7 ++++--- types/SplitTreatments.d.ts | 5 +++-- types/types.d.ts | 14 +++++++++++--- types/useSplitTreatments.d.ts | 5 +++-- types/utils.d.ts | 2 +- 10 files changed, 61 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f01255..455ce50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.3", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -1547,11 +1547,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", + "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", "dependencies": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1569,9 +1569,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "dependencies": { "tslib": "^2.3.1" }, @@ -12089,11 +12089,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", + "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", "requires": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -12105,9 +12105,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "requires": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index 206ca9b..cb69ffc 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.3", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 563b2cf..6049c5e 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -5,8 +5,9 @@ import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants'; import { memoizeGetTreatmentsWithConfig } from './utils'; /** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. + * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to + * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method + * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ @@ -18,7 +19,7 @@ export class SplitTreatments extends React.Component { private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig(); render() { - const { names, children, attributes } = this.props; + const { names, flagSets, children, attributes } = this.props; return ( @@ -29,7 +30,7 @@ export class SplitTreatments extends React.Component { if (client && isOperational) { // Cloning `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. // Caveat: same issue happens with `names` and `attributes` props if the user follows the bad practice of mutating the object instead of providing a new one. - treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }); + treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets); } else { treatments = getControlTreatmentsWithConfig(names); if (!client) { this.logWarning = true; } diff --git a/src/types.ts b/src/types.ts index 42d2a85..7654fdf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -172,14 +172,19 @@ export interface ISplitClientProps extends IUseSplitClientOptions { /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names */ - names: string[] + names?: string[]; + + /** + * list of feature flag sets + */ + flagSets?: string[]; } /** @@ -207,7 +212,12 @@ export interface ISplitTreatmentsProps { /** * list of feature flag names */ - names: string[]; + names?: string[]; + + /** + * list of feature flag sets + */ + flagSets?: string[]; /** * An object of type Attributes used to evaluate the feature flags. diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index ddc0590..1e95ab2 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -5,8 +5,9 @@ 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). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. + * 'useSplitTreatments' is a hook that returns a SplitContext object extended with a `treatments` property, which contains an object of feature flag evaluations (i.e., treatments). + * It utilizes the 'useSplitClient' hook to access the client from the context and invokes the 'getTreatmentsWithConfig' method + * if `names` property is provided, or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` property is provided. * * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} @@ -14,12 +15,12 @@ import { useSplitClient } from './useSplitClient'; export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { const context = useSplitClient({...options, attributes: undefined }); const { client, lastUpdate } = context; - const { names, attributes } = options; + const { names, flagSets, attributes } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }) : + getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets) : getControlTreatmentsWithConfig(names); return { diff --git a/src/utils.ts b/src/utils.ts index d3ca36a..d6a892c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -176,9 +176,10 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { newArgs[1] === lastArgs[1] && // lastUpdate shallowEqual(newArgs[2], lastArgs[2]) && // names shallowEqual(newArgs[3], lastArgs[3]) && // attributes - shallowEqual(newArgs[4], lastArgs[4]); // client attributes + shallowEqual(newArgs[4], lastArgs[4]) && // client attributes + shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) { - return client.getTreatmentsWithConfig(names, attributes); +function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { + return names ? client.getTreatmentsWithConfig(names, attributes) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes); } diff --git a/types/SplitTreatments.d.ts b/types/SplitTreatments.d.ts index 70e213a..a66ff78 100644 --- a/types/SplitTreatments.d.ts +++ b/types/SplitTreatments.d.ts @@ -1,8 +1,9 @@ import React from 'react'; import { ISplitTreatmentsProps } from './types'; /** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. + * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to + * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method + * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ diff --git a/types/types.d.ts b/types/types.d.ts index 0e4c275..6e06366 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -141,13 +141,17 @@ export interface ISplitClientProps extends IUseSplitClientOptions { } /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names */ - names: string[]; + names?: string[]; + /** + * list of feature flag sets + */ + flagSets?: string[]; } /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -171,7 +175,11 @@ export interface ISplitTreatmentsProps { /** * list of feature flag names */ - names: string[]; + names?: string[]; + /** + * list of feature flag sets + */ + flagSets?: string[]; /** * An object of type Attributes used to evaluate the feature flags. */ diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts index b636632..bf7a5ff 100644 --- a/types/useSplitTreatments.d.ts +++ b/types/useSplitTreatments.d.ts @@ -1,7 +1,8 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; /** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. + * 'useSplitTreatments' is a hook that returns a SplitContext object extended with a `treatments` property, which contains an object of feature flag evaluations (i.e., treatments). + * It utilizes the 'useSplitClient' hook to access the client from the context and invokes the 'getTreatmentsWithConfig' method + * if `names` property is provided, or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` property is provided. * * @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. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} diff --git a/types/utils.d.ts b/types/utils.d.ts index 942eb0d..0465872 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -34,5 +34,5 @@ export declare function initAttributes(client: SplitIO.IBrowserClient | null, at * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. */ export declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; -declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; +declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; export {}; From 601f7dccfac94356f55bad75119cf728909e7aff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 18:23:41 -0300 Subject: [PATCH 43/67] using union to restrict that either 'names' or 'flagSets' is required but not both --- src/__tests__/SplitTreatments.test.tsx | 8 +++--- src/types.ts | 35 +++++++++++++++----------- types/types.d.ts | 35 +++++++++++++++----------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index f3c2f56..52fba21 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -150,16 +150,16 @@ let renderTimes = 0; * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments. */ describe.each([ - ({ names, attributes }) => ( - + ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => ( + {() => { renderTimes++; return null; }} ), - ({ names, attributes }) => { - useSplitTreatments({ names, attributes }); + ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => { + useSplitTreatments({ names, attributes, flagSets: undefined }); renderTimes++; return null; } diff --git a/src/types.ts b/src/types.ts index 7654fdf..8f9fe1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,18 +174,21 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ /** * list of feature flag names */ - names?: string[]; + names: string[]; + flagSets?: undefined; +} | { /** * list of feature flag sets */ - flagSets?: string[]; -} + flagSets: string[]; + names?: undefined; +}) /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -207,25 +210,29 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. */ -export interface ISplitTreatmentsProps { +export type ISplitTreatmentsProps = { /** - * list of feature flag names + * An object of type Attributes used to evaluate the feature flags. */ - names?: string[]; + attributes?: SplitIO.Attributes; /** - * list of feature flag sets + * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ - flagSets?: string[]; + children: ((props: ISplitTreatmentsChildProps) => ReactNode); +} & ({ /** - * An object of type Attributes used to evaluate the feature flags. + * list of feature flag names */ - attributes?: SplitIO.Attributes; + names: string[]; + flagSets?: undefined; +} | { /** - * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. + * list of feature flag sets */ - children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} + flagSets: string[]; + names?: undefined; +}) diff --git a/types/types.d.ts b/types/types.d.ts index 6e06366..8f0caf3 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -143,16 +143,19 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ /** * list of feature flag names */ - names?: string[]; + names: string[]; + flagSets?: undefined; +} | { /** * list of feature flag sets */ - flagSets?: string[]; -} + flagSets: string[]; + names?: undefined; +}); /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -171,15 +174,7 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. */ -export interface ISplitTreatmentsProps { - /** - * list of feature flag names - */ - names?: string[]; - /** - * list of feature flag sets - */ - flagSets?: string[]; +export declare type ISplitTreatmentsProps = { /** * An object of type Attributes used to evaluate the feature flags. */ @@ -188,4 +183,16 @@ export interface ISplitTreatmentsProps { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} +} & ({ + /** + * list of feature flag names + */ + names: string[]; + flagSets?: undefined; +} | { + /** + * list of feature flag sets + */ + flagSets: string[]; + names?: undefined; +}); From fe258598614edc7574354080907212fa74e45387 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 10 Nov 2023 18:07:20 -0300 Subject: [PATCH 44/67] update tests to avoid duplicated --- src/__tests__/useClient.test.tsx | 99 +----- src/__tests__/useSplitClient.test.tsx | 359 +++++++++++++--------- src/__tests__/useSplitTreatments.test.tsx | 292 +++++++++++++----- src/__tests__/useTreatments.test.tsx | 157 +--------- src/utils.ts | 2 +- 5 files changed, 463 insertions(+), 446 deletions(-) diff --git a/src/__tests__/useClient.test.tsx b/src/__tests__/useClient.test.tsx index 5bcc4da..544e843 100644 --- a/src/__tests__/useClient.test.tsx +++ b/src/__tests__/useClient.test.tsx @@ -1,100 +1,23 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; +const useSplitClientMock = jest.fn(); +jest.mock('../useSplitClient', () => ({ + useSplitClient: useSplitClientMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; -import { SplitClient } from '../SplitClient'; import { useClient } from '../useClient'; -import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('useClient', () => { - test('returns the main client from the context updated by SplitFactory.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - {React.createElement(() => { - client = useClient(); - return null; - })} - - ); - expect(client).toBe(outerFactory.client()); - }); - - test('returns the client from the context updated by SplitClient.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - - {React.createElement(() => { - client = useClient(); - return null; - })} - - - ); - expect(client).toBe(outerFactory.client('user2')); - }); - - test('returns a new client from the factory at Split context given a splitKey.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - {React.createElement(() => { - (outerFactory.client as jest.Mock).mockClear(); - client = useClient('user2', 'user'); - return null; - })} - - ); - expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); - expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); - }); - - test('returns null if invoked outside Split context.', () => { - let client; - let sharedClient; - render( - React.createElement(() => { - client = useClient(); - sharedClient = useClient('user2', 'user'); - return null; - }) - ); - expect(client).toBe(null); - expect(sharedClient).toBe(null); - }); - - test('attributes binding test with utility', (done) => { - - // eslint-disable-next-line react/prop-types - const InnerComponent = ({ splitKey, attributesClient, testSwitch}) => { - useClient(splitKey, 'user', attributesClient); - testSwitch(done, splitKey); - return null; - }; + test('calls useSplitClient with the correct arguments and returns the client.', () => { + const attributes = { someAttribute: 'someValue' }; + const client = 'client'; + useSplitClientMock.mockReturnValue({ client, isReady: false }); - function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { - return ( - - - - ); - } + expect(useClient('someKey', 'someTrafficType', attributes)).toBe(client); - testAttributesBinding(Component); + expect(useSplitClientMock).toHaveBeenCalledTimes(1); + expect(useSplitClientMock).toHaveBeenCalledWith({ splitKey: 'someKey', trafficType: 'someTrafficType', attributes }); }); }); diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 8551c7b..3897182 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -10,165 +10,250 @@ import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ -import { SplitFactory } from '../SplitFactory'; import { useSplitClient } from '../useSplitClient'; +import { SplitFactory } from '../SplitFactory'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; +import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; -test('useSplitClient must update on SDK events', () => { - const outerFactory = SplitSdk(sdkBrowser); - const mainClient = outerFactory.client() as any; - const user2Client = outerFactory.client('user_2') as any; - - let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; - let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0; - let countNestedComponent = 0; - - render( - - <> - - {() => countSplitContext++} - - - {() => { countSplitClient++; return null }} - - {React.createElement(() => { - // Equivalent to - // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` - // - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` - const { client } = useSplitClient({ attributes: { att1: 'att1' } }); - expect(client).toBe(mainClient); // Assert that the main client was retrieved. - expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. - countUseSplitClient++; - return null; - })} - - {() => { countSplitClientUser2++; return null }} - - {React.createElement(() => { - const { client } = useSplitClient({ splitKey: 'user_2' }); - expect(client).toBe(user2Client); - countUseSplitClientUser2++; - return null; - })} - - {() => { countSplitClientWithUpdate++; return null }} - +describe('useSplitClient', () => { + + test('returns the main client from the context updated by SplitFactory.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + {React.createElement(() => { - useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; - countUseSplitClientWithUpdate++; + client = useSplitClient().client; return null; })} - - {() => { countSplitClientUser2WithUpdate++; return null }} + + ); + expect(client).toBe(outerFactory.client()); + }); + + test('returns the client from the context updated by SplitClient.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + + + {React.createElement(() => { + client = useSplitClient().client; + return null; + })} + + ); + expect(client).toBe(outerFactory.client('user2')); + }); + + test('returns a new client from the factory at Split context given a splitKey.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + {React.createElement(() => { - useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); - countUseSplitClientUser2WithUpdate++; + (outerFactory.client as jest.Mock).mockClear(); + client = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; return null; })} - + + ); + expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); + expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); + }); + + test('returns null if invoked outside Split context.', () => { + let client; + let sharedClient; + render( + React.createElement(() => { + client = useSplitClient().client; + sharedClient = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; + return null; + }) + ); + expect(client).toBe(null); + expect(sharedClient).toBe(null); + }); + + test('attributes binding test with utility', (done) => { + + // eslint-disable-next-line react/prop-types + const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => { + useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient}); + testSwitch(done, splitKey); + return null; + }; + + function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { + return ( + + + + ); + } + + testAttributesBinding(Component); + }); + + test('useSplitClient must update on SDK events', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; + let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0; + let countNestedComponent = 0; + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitClient++; return null }} + {React.createElement(() => { - const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); - expect(status.client).toBe(user2Client); - - // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event - countNestedComponent++; - switch (countNestedComponent) { - case 1: - expect(status.isReady).toBe(false); - expect(status.isReadyFromCache).toBe(false); - break; - case 2: - expect(status.isReady).toBe(false); - expect(status.isReadyFromCache).toBe(true); - break; - case 3: - expect(status.isReady).toBe(true); - expect(status.isReadyFromCache).toBe(true); - break; - case 4: - break; - default: - throw new Error('Unexpected render'); - } + // Equivalent to + // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` + // - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` + const { client } = useSplitClient({ attributes: { att1: 'att1' } }); + expect(client).toBe(mainClient); // Assert that the main client was retrieved. + expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. + countUseSplitClient++; 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)); - - // SplitContext renders 3 times: initially, when ready from cache, and when ready. - expect(countSplitContext).toEqual(3); - - // If SplitClient and useSplitClient retrieve the same client than the context and have default update options, - // they render when the context renders. - expect(countSplitClient).toEqual(countSplitContext); - expect(countUseSplitClient).toEqual(countSplitContext); - - // If SplitClient and useSplitClient retrieve a different client than the context and have default update options, - // they render when the context renders and when the new client is ready and ready from cache. - expect(countSplitClientUser2).toEqual(countSplitContext + 2); - expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); - - // If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true, - // they render when the context renders and when the client updates. - expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1); - expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1); - - // If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true, - // they render when the context renders and when the new client is ready, ready from cache and updates. - expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); - expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); - - expect(countNestedComponent).toEqual(4); -}); + + {() => { countSplitClientUser2++; return null }} + + {React.createElement(() => { + const { client } = useSplitClient({ splitKey: 'user_2' }); + expect(client).toBe(user2Client); + countUseSplitClientUser2++; + return null; + })} + + {() => { countSplitClientWithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; + countUseSplitClientWithUpdate++; + return null; + })} + + {() => { countSplitClientUser2WithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); + countUseSplitClientUser2WithUpdate++; + return null; + })} + + {React.createElement(() => { + const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); + expect(status.client).toBe(user2Client); -test('useSplitClient must support changes in update props', () => { - const outerFactory = SplitSdk(sdkBrowser); - const mainClient = outerFactory.client() as any; + // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event + countNestedComponent++; + switch (countNestedComponent) { + case 1: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(false); + break; + case 2: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(true); + break; + case 3: + expect(status.isReady).toBe(true); + expect(status.isReadyFromCache).toBe(true); + break; + case 4: + break; + default: + throw new Error('Unexpected render'); + } + return null; + })} + + + + ); - let rendersCount = 0; + 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)); - function InnerComponent(updateOptions) { - useSplitClient(updateOptions); - rendersCount++; - return null; - } + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); - function Component(updateOptions) { - return ( - - - - ) - } + // If SplitClient and useSplitClient retrieve the same client than the context and have default update options, + // they render when the context renders. + expect(countSplitClient).toEqual(countSplitContext); + expect(countUseSplitClient).toEqual(countSplitContext); + + // If SplitClient and useSplitClient retrieve a different client than the context and have default update options, + // they render when the context renders and when the new client is ready and ready from cache. + expect(countSplitClientUser2).toEqual(countSplitContext + 2); + expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); + + // If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the client updates. + expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1); + expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1); + + // If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the new client is ready, ready from cache and updates. + expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + + expect(countNestedComponent).toEqual(4); + }); + + test('useSplitClient must support changes in update props', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + + let rendersCount = 0; + + function InnerComponent(updateOptions) { + useSplitClient(updateOptions); + rendersCount++; + return null; + } + + function Component(updateOptions) { + return ( + + + + ) + } + + const wrapper = render(); - const wrapper = render(); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default + expect(rendersCount).toBe(2); - act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default - expect(rendersCount).toBe(2); + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now - wrapper.rerender(); // trigger re-render - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now + expect(rendersCount).toBe(4); - expect(rendersCount).toBe(4); + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now - wrapper.rerender(); // trigger re-render - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now + expect(rendersCount).toBe(5); + }); - expect(rendersCount).toBe(5); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 5fe57a9..f5000d6 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -8,93 +8,235 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; +jest.mock('../constants', () => { + const actual = jest.requireActual('../constants'); + return { + ...actual, + getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), + }; +}); +import { CONTROL_WITH_CONFIG, getControlTreatmentsWithConfig } from '../constants'; +const logSpy = jest.spyOn(console, 'log'); /** Test target */ import { SplitFactory } from '../SplitFactory'; +import { SplitClient } from '../SplitClient'; import { useSplitTreatments } from '../useSplitTreatments'; import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; -function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { - if (isReady || isReadyFromCache) { - expect(treatments).toEqual({ - split_test: { - treatment: 'on', - config: null, - } - }) - } else { - expect(treatments).toEqual({ - split_test: { - treatment: 'control', - config: null, - } - }) - } -} - -test('useSplitTreatments must update on SDK events', async () => { - const outerFactory = SplitSdk(sdkBrowser); - const mainClient = outerFactory.client() as any; - const user2Client = outerFactory.client('user_2') as any; - - let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; - - render( - - <> - - {() => countSplitContext++} - - - {() => { countSplitTreatments++; return null }} - - {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); - expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. - validateTreatments(context); - countUseSplitTreatments++; - return null; - })} +describe('useSplitTreatments', () => { + + const featureFlagNames = ['split1']; + const attributes = { att1: 'att1' }; + + test('returns the treatments evaluated by the client at Split context updated by SplitFactory, or control if the client is not operational.', () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client(); + let treatments: SplitIO.TreatmentsWithConfig; + + render( + {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); - expect(context.client).toBe(user2Client); - validateTreatments(context); - countUseSplitTreatmentsUser2++; + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; return null; })} + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + + // once operational (SDK_READY), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY)); + + expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + }); + + test('returns the Treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client('user2'); + let treatments: SplitIO.TreatmentsWithConfig; + + render( + + + {React.createElement(() => { + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + return null; + })} + + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + + // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + + expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + }); + + test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client('user2'); + let renderTimes = 0; + + render( + {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true }); - expect(context.client).toBe(user2Client); - validateTreatments(context); - countUseSplitTreatmentsUser2WithUpdate++; + const treatments = useSplitTreatments({ names: featureFlagNames, attributes, splitKey: 'user2' }).treatments; + + renderTimes++; + switch (renderTimes) { + case 1: + // returns control if not operational (SDK not ready), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + break; + case 2: + case 3: + // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags + expect(client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveLastReturnedWith(treatments); + break; + default: + throw new Error('Unexpected render'); + } + 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)); - - // SplitContext renders 3 times: initially, when ready from cache, and when ready. - expect(countSplitContext).toEqual(3); - - // SplitTreatments and useSplitTreatments render when the context renders. - expect(countSplitTreatments).toEqual(countSplitContext); - expect(countUseSplitTreatments).toEqual(countSplitContext); - expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(4); - expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }); - - // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. - expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); - // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. - expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); - expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); - expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); + + ); + + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => client.__emitter__.emit(Event.SDK_READY)); + act(() => client.__emitter__.emit(Event.SDK_UPDATE)); // should not trigger a re-render by default + expect(client.getTreatmentsWithConfig).toBeCalledTimes(2); + }); + + // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' + test('returns Control Treatments if invoked outside Split context.', () => { + let treatments; + + render( + React.createElement(() => { + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + return null; + }) + ); + expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); + expect(getControlTreatmentsWithConfig).toHaveReturnedWith(treatments); + }); + + /** + * Input validation. Passing invalid feature flag names or attributes while the Sdk + * is not ready doesn't emit errors, and logs meaningful messages instead. + */ + test('Input validation: invalid "names" and "attributes" params in useSplitTreatments.', () => { + render( + React.createElement(() => { + // @ts-expect-error Test error handling + let treatments = useSplitTreatments('split1').treatments; + expect(treatments).toEqual({}); + // @ts-expect-error Test error handling + treatments = useSplitTreatments({ names: [true] }).treatments; + expect(treatments).toEqual({}); + + 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.'); + }); + + test('useSplitTreatments must update on SDK events', async () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; + + function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { + if (isReady || isReadyFromCache) { + expect(treatments).toEqual({ + split_test: { + treatment: 'on', + config: null, + } + }) + } else { + expect(treatments).toEqual({ + split_test: { + treatment: 'control', + config: null, + } + }) + } + } + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitTreatments++; return null }} + + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); + expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. + validateTreatments(context); + countUseSplitTreatments++; + return null; + })} + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); + expect(context.client).toBe(user2Client); + validateTreatments(context); + countUseSplitTreatmentsUser2++; + return null; + })} + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true }); + expect(context.client).toBe(user2Client); + validateTreatments(context); + countUseSplitTreatmentsUser2WithUpdate++; + 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)); + + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); + + // SplitTreatments and useSplitTreatments render when the context renders. + expect(countSplitTreatments).toEqual(countSplitContext); + expect(countUseSplitTreatments).toEqual(countSplitContext); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(4); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }); + + // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. + expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); + // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. + expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); + }); + }); diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx index 2116f92..b375d05 100644 --- a/src/__tests__/useTreatments.test.tsx +++ b/src/__tests__/useTreatments.test.tsx @@ -1,157 +1,24 @@ -import React from 'react'; -import { render, act } from '@testing-library/react'; - /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { CONTROL_WITH_CONFIG, getControlTreatmentsWithConfig } from '../constants'; -const logSpy = jest.spyOn(console, 'log'); +const useSplitTreatmentsMock = jest.fn(); +jest.mock('../useSplitTreatments', () => ({ + useSplitTreatments: useSplitTreatmentsMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; -import { SplitClient } from '../SplitClient'; import { useTreatments } from '../useTreatments'; describe('useTreatments', () => { - const featureFlagNames = ['split1']; - const attributes = { att1: 'att1' }; - - test('returns the treatments evaluated by the client at Split context updated by SplitFactory, or control if the client is not operational.', () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client(); - let treatments: SplitIO.TreatmentsWithConfig; - - render( - - {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - })} - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); - - // once operational (SDK_READY), it evaluates feature flags - act(() => client.__emitter__.emit(Event.SDK_READY)); - - expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); - - test('returns the Treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client('user2'); - let treatments: SplitIO.TreatmentsWithConfig; - - render( - - - {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - })} - - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); - - // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags - act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - - expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); - - test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client('user2'); - let renderTimes = 0; - - render( - - {React.createElement(() => { - const treatments = useTreatments(featureFlagNames, attributes, 'user2'); - - renderTimes++; - switch (renderTimes) { - case 1: - // returns control if not operational (SDK not ready), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); - break; - case 2: - case 3: - // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags - expect(client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).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)); // should not trigger a re-render by default - expect(client.getTreatmentsWithConfig).toBeCalledTimes(2); - }); - - // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns Control Treatments if invoked outside Split context.', () => { - let treatments; - - render( - React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - }) - ); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); + test('calls useSplitTreatments with the correct arguments and returns the treatments.', () => { + const names = ['someFeature']; + const attributes = { someAttribute: 'someValue' }; + const treatments = { someFeature: { treatment: 'on', config: null } }; + useSplitTreatmentsMock.mockReturnValue({ treatments, isReady: false }); - /** - * Input validation. Passing invalid feature flag names or attributes while the Sdk - * is not ready doesn't emit errors, and logs meaningful messages instead. - */ - test('Input validation: invalid "names" and "attributes" params in useTreatments.', (done) => { - render( - React.createElement(() => { - // @ts-expect-error Test error handling - let treatments = useTreatments('split1'); - expect(treatments).toEqual({}); - // @ts-expect-error Test error handling - treatments = useTreatments([true]); - expect(treatments).toEqual({}); + expect(useTreatments(names, attributes, 'someKey')).toBe(treatments); - done(); - 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(useSplitTreatmentsMock).toHaveBeenCalledTimes(1); + expect(useSplitTreatmentsMock).toHaveBeenCalledWith({ names, attributes, splitKey: 'someKey' }); }); }); diff --git a/src/utils.ts b/src/utils.ts index d3ca36a..5d48611 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,7 +49,7 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC // idempotent operation export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation - const client = (key ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + const client = (key !== undefined ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; // Handle client lastUpdate if (client.lastUpdate === undefined) { From f8c13c340755cc6ee776215e8c70c843ba5d0d1c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 10 Nov 2023 18:16:16 -0300 Subject: [PATCH 45/67] update useManager test to avoid duplication of tests --- src/__tests__/useManager.test.tsx | 42 ++++++++----------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/src/__tests__/useManager.test.tsx b/src/__tests__/useManager.test.tsx index 041c7b4..ba4c36c 100644 --- a/src/__tests__/useManager.test.tsx +++ b/src/__tests__/useManager.test.tsx @@ -1,43 +1,21 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; +const useSplitManagerMock = jest.fn(); +jest.mock('../useSplitManager', () => ({ + useSplitManager: useSplitManagerMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; import { useManager } from '../useManager'; describe('useManager', () => { - test('returns the factory manager from the Split context.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let manager; - render( - - {React.createElement(() => { - manager = useManager(); - return null; - })} - - ); - expect(manager).toBe(outerFactory.manager()); - }); + test('calls useSplitManager with the correct arguments and returns the manager.', () => { + const manager = 'manager'; + useSplitManagerMock.mockReturnValue({ manager, isReady: false }); + + expect(useManager()).toBe(manager); - test('returns null if invoked outside Split context.', () => { - let manager; - render( - React.createElement(() => { - manager = useManager(); - return null; - }) - ); - expect(manager).toBe(null); + expect(useSplitManagerMock).toHaveBeenCalledTimes(1); }); }); From fe02e0595f2a74c26f38ff1e68bf06f3630d783e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 13:52:54 -0300 Subject: [PATCH 46/67] Add tests --- src/SplitTreatments.tsx | 16 +-- src/__tests__/SplitTreatments.test.tsx | 139 ++++++++++++++-------- src/__tests__/testUtils/mockSplitSdk.ts | 7 ++ src/__tests__/useSplitTreatments.test.tsx | 52 +++++--- src/constants.ts | 6 +- src/types.ts | 21 ++-- src/useSplitTreatments.ts | 11 +- src/utils.ts | 14 ++- types/constants.d.ts | 1 + types/types.d.ts | 21 ++-- types/utils.d.ts | 2 +- 11 files changed, 168 insertions(+), 122 deletions(-) diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 6049c5e..fd89a7a 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitTreatmentsProps, ISplitContextValues } from './types'; -import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants'; +import { WARN_ST_NO_CLIENT } from './constants'; import { memoizeGetTreatmentsWithConfig } from './utils'; /** @@ -24,17 +24,9 @@ export class SplitTreatments extends React.Component { return ( {(splitContext: ISplitContextValues) => { - const { client, isReady, isReadyFromCache, isDestroyed, lastUpdate } = splitContext; - let treatments; - const isOperational = !isDestroyed && (isReady || isReadyFromCache); - if (client && isOperational) { - // Cloning `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. - // Caveat: same issue happens with `names` and `attributes` props if the user follows the bad practice of mutating the object instead of providing a new one. - treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets); - } else { - treatments = getControlTreatmentsWithConfig(names); - if (!client) { this.logWarning = true; } - } + const { client, lastUpdate } = splitContext; + const treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); + if (!client) { this.logWarning = true; } // SplitTreatments only accepts a function as a child, not a React Element (JSX) return children({ ...splitContext, treatments, diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 52fba21..ca20da9 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -8,31 +8,27 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -const logSpy = jest.spyOn(console, 'log'); +import { getStatus } from '../utils'; +import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; +import { CONTROL_WITH_CONFIG, WARN_ST_NO_CLIENT } from '../constants'; /** Test target */ import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types'; import { SplitTreatments } from '../SplitTreatments'; import { SplitClient } from '../SplitClient'; import { SplitFactory } from '../SplitFactory'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from '../constants'; -import { getStatus } from '../utils'; -import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; import { useSplitTreatments } from '../useSplitTreatments'; +const logSpy = jest.spyOn(console, 'log'); + describe('SplitTreatments', () => { + const featureFlagNames = ['split1', 'split2']; + const flagSets = ['set1', 'set2']; + afterEach(() => { logSpy.mockClear() }); - it('passes as treatments prop the value returned by the function "getControlTreatmentsWithConfig" if the SDK is not ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes control treatments (empty object if flagSets is provided) if the SDK is not ready.', () => { render( {({ factory }) => { @@ -41,9 +37,16 @@ describe('SplitTreatments', () => { {({ treatments }: ISplitTreatmentsChildProps) => { const clientMock: any = factory?.client('user1'); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(0); - expect(treatments).toEqual(getControlTreatmentsWithConfig(featureFlagNames)); - done(); + expect(clientMock.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client('user1'); + expect(clientMock.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatments).toEqual({}); return null; }} @@ -54,8 +57,7 @@ describe('SplitTreatments', () => { ); }); - it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig" if the SDK is ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig(ByFlagSets)" if the SDK is ready.', () => { const outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); @@ -65,37 +67,44 @@ describe('SplitTreatments', () => { expect(getStatus(outerFactory.client()).isReady).toBe(isReady); expect(isReady).toBe(true); return ( - - {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client(); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); - expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); - expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); - expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]); - done(); - return null; - }} - + <> + + {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); + expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); + expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.results[0].value); + expect(flagSets).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls[0][0]); + return null; + }} + + ); }} ); }); - it('logs error and passes control treatments ("getControlTreatmentsWithConfig") if rendered outside an SplitProvider component.', () => { - const featureFlagNames = ['split1', 'split2']; - let passedTreatments; + it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { render( {({ treatments }: ISplitTreatmentsChildProps) => { - passedTreatments = treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); return null; }} ); + expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(passedTreatments); }); /** @@ -103,8 +112,6 @@ describe('SplitTreatments', () => { * is not ready doesn't emit errors, and logs meaningful messages instead. */ it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { - const featureFlagNames = ['split1', 'split2']; - render( {() => { @@ -142,24 +149,37 @@ describe('SplitTreatments', () => { done(); }); + + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + + {({ treatments }) => { + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + ); + + expect(logSpy).toBeCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + }); }); let renderTimes = 0; /** - * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments. + * Tests for asserting that client.getTreatmentsWithConfig and client.getTreatmentsWithConfigByFlagSets are not called unnecessarily when using SplitTreatments and useSplitTreatments. */ describe.each([ - ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => ( - + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => ( + {() => { renderTimes++; return null; }} ), - ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => { - useSplitTreatments({ names, attributes, flagSets: undefined }); + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => { + useSplitTreatments({ names, flagSets, attributes }); renderTimes++; return null; } @@ -167,8 +187,9 @@ describe.each([ let outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - function Component({ names, attributes, splitKey, clientAttributes }: { - names: ISplitTreatmentsProps['names'] + function Component({ names, flagSets, attributes, splitKey, clientAttributes }: { + names?: ISplitTreatmentsProps['names'] + flagSets?: ISplitTreatmentsProps['flagSets'] attributes: ISplitTreatmentsProps['attributes'] splitKey: ISplitClientProps['splitKey'] clientAttributes?: ISplitClientProps['attributes'] @@ -176,13 +197,14 @@ describe.each([ return ( - + ); } const names = ['split1', 'split2']; + const flagSets = ['set1', 'set2']; const attributes = { att1: 'att1' }; const splitKey = sdkBrowser.core.key; @@ -191,25 +213,27 @@ describe.each([ beforeEach(() => { renderTimes = 0; (outerFactory.client().getTreatmentsWithConfig as jest.Mock).mockClear(); - wrapper = render(); + wrapper = render(); }) afterEach(() => { wrapper.unmount(); // unmount to remove event listener from factory }) - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are the same object.', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are the same object.', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are equals (shallow comparison).', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are equals (shallow comparison).', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); it('rerenders and re-evaluates feature flags if names are not equals (shallow array comparison).', () => { @@ -217,6 +241,19 @@ describe.each([ expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); + }); + + it('rerenders and re-evaluates feature flags if flag sets are not equals (shallow array comparison).', () => { + wrapper.rerender(); + wrapper.rerender(); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(1); + + wrapper.rerender(); + + expect(renderTimes).toBe(4); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(2); }); it('rerenders and re-evaluates feature flags if attributes are not equals (shallow object comparison).', () => { @@ -265,8 +302,6 @@ describe.each([ it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => { // changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments const outerFactory = SplitSdk(sdkBrowser); - const names = ['split1', 'split2']; - const attributes = { att1: 'att1' }; let renderTimesComp1 = 0; let renderTimesComp2 = 0; diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index 846e104..22d40ae 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -53,6 +53,12 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { return result; }, {}); }); + const getTreatmentsWithConfigByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => { + return flagSets.reduce((result: SplitIO.TreatmentsWithConfig, flagSet: string) => { + result[flagSet + '_feature_flag'] = { treatment: 'on', config: null }; + return result; + }, {}); + }); const setAttributes: jest.Mock = jest.fn((attributes) => { attributesCache = Object.assign(attributesCache, attributes); return true; @@ -87,6 +93,7 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { return Object.assign(Object.create(__emitter__), { getTreatmentsWithConfig, + getTreatmentsWithConfigByFlagSets, track, ready, destroy, diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index f5000d6..6699db3 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -8,15 +8,7 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { CONTROL_WITH_CONFIG, getControlTreatmentsWithConfig } from '../constants'; -const logSpy = jest.spyOn(console, 'log'); +import { CONTROL_WITH_CONFIG } from '../constants'; /** Test target */ import { SplitFactory } from '../SplitFactory'; @@ -26,20 +18,25 @@ import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; +const logSpy = jest.spyOn(console, 'log'); + describe('useSplitTreatments', () => { const featureFlagNames = ['split1']; + const flagSets = ['set1']; const attributes = { att1: 'att1' }; - test('returns the treatments evaluated by the client at Split context updated by SplitFactory, or control if the client is not operational.', () => { + test('returns the treatments evaluated by the client at Split context, or control if the client is not operational.', () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client(); let treatments: SplitIO.TreatmentsWithConfig; + let treatmentsByFlagSets: SplitIO.TreatmentsWithConfig; render( {React.createElement(() => { treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; return null; })} @@ -49,14 +46,21 @@ describe('useSplitTreatments', () => { expect(client.getTreatmentsWithConfig).not.toBeCalled(); expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + // returns empty treatments object if not operational, without calling `getTreatmentsWithConfigByFlagSets` method + expect(client.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatmentsByFlagSets!).toEqual({}); + // once operational (SDK_READY), it evaluates feature flags act(() => client.__emitter__.emit(Event.SDK_READY)); expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + + expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith(flagSets, attributes); + expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets); }); - test('returns the Treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { + test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client('user2'); let treatments: SplitIO.TreatmentsWithConfig; @@ -83,7 +87,7 @@ describe('useSplitTreatments', () => { expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); }); - test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { + test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client('user2'); let renderTimes = 0; @@ -122,17 +126,17 @@ describe('useSplitTreatments', () => { }); // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns Control Treatments if invoked outside Split context.', () => { - let treatments; - + test('returns control treatments (empty object if flagSets is provided) if invoked outside Split context.', () => { render( React.createElement(() => { - treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + const treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + + const treatmentsByFlagSets = useSplitTreatments({ flagSets: featureFlagNames }).treatments; + expect(treatmentsByFlagSets).toEqual({}); return null; }) ); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(treatments); }); /** @@ -239,4 +243,16 @@ describe('useSplitTreatments', () => { expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + React.createElement(() => { + const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + return null; + }) + ); + + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + }); + }); diff --git a/src/constants.ts b/src/constants.ts index 978dc58..c5675d0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,12 +30,14 @@ export const getControlTreatmentsWithConfig = (featureFlagNames: unknown): Split }; // Warning and error messages -export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; +export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; export const ERROR_SF_NO_CONFIG_AND_FACTORY: string = '[ERROR] SplitFactory must receive either a Split config or a Split factory as props.'; export const ERROR_SC_NO_FACTORY: string = '[ERROR] SplitClient does not have access to a Split factory. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; -export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; +export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; + +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 8f9fe1e..7746369 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,21 +174,18 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ +export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}) + flagSets?: string[]; +} /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -221,18 +218,14 @@ export type ISplitTreatmentsProps = { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} & ({ /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}) + flagSets?: string[]; +} diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 1e95ab2..1be4757 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,6 +1,5 @@ import React from 'react'; -import { getControlTreatmentsWithConfig } from './constants'; -import { IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils'; +import { memoizeGetTreatmentsWithConfig } from './utils'; import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; @@ -13,15 +12,15 @@ import { useSplitClient } from './useSplitClient'; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { - const context = useSplitClient({...options, attributes: undefined }); + const context = useSplitClient({ ...options, attributes: undefined }); const { client, lastUpdate } = context; const { names, flagSets, attributes } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); - const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets) : - getControlTreatmentsWithConfig(names); + // Clone `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. + // Note: the same issue occurs with `names` and `attributes` arguments if the user mutates them directly instead of providing a new object. + const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); return { ...context, diff --git a/src/utils.ts b/src/utils.ts index a302f79..3cffbc0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import memoizeOne from 'memoize-one'; import shallowEqual from 'shallowequal'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { VERSION } from './constants'; +import { VERSION, WARN_NAMES_AND_FLAGSETS, getControlTreatmentsWithConfig } from './constants'; import { ISplitStatus } from './types'; // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. @@ -180,6 +180,14 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { - return names ? client.getTreatmentsWithConfig(names, attributes) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes); +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { + if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS); + + return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? + names ? + client.getTreatmentsWithConfig(names, attributes) : + client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes) : + names ? + getControlTreatmentsWithConfig(names) : + {} } diff --git a/types/constants.d.ts b/types/constants.d.ts index e382449..e2e823c 100644 --- a/types/constants.d.ts +++ b/types/constants.d.ts @@ -9,3 +9,4 @@ export declare const ERROR_SF_NO_CONFIG_AND_FACTORY: string; export declare const ERROR_SC_NO_FACTORY: string; export declare const WARN_ST_NO_CLIENT: string; export declare const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string; +export declare const WARN_NAMES_AND_FLAGSETS: string; diff --git a/types/types.d.ts b/types/types.d.ts index 8f0caf3..f87f867 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -143,19 +143,16 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ +export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}); + flagSets?: string[]; +}; /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -183,16 +180,12 @@ export declare type ISplitTreatmentsProps = { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} & ({ /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}); + flagSets?: string[]; +}; diff --git a/types/utils.d.ts b/types/utils.d.ts index 0465872..f0005cf 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -34,5 +34,5 @@ export declare function initAttributes(client: SplitIO.IBrowserClient | null, at * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. */ export declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; -declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; +declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; export {}; From 50299e7cb2101a6558b2e4c51181c8f531159929 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 14:46:15 -0300 Subject: [PATCH 47/67] Add test to validate that the useSplitManager hook updates when the context change (e.g., SDK is ready) --- src/__tests__/useSplitManager.test.tsx | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index 807f068..9ec5367 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; +import { Event, mockSdk } from './testUtils/mockSplitSdk'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; +import { getStatus } from '../utils'; /** Test target */ import { SplitFactory } from '../SplitFactory'; @@ -15,7 +16,7 @@ import { useSplitManager } from '../useSplitManager'; describe('useSplitManager', () => { - test('returns the factory manager from the Split context.', () => { + test('returns the factory manager from the Split context, and updates when the context changes.', () => { const outerFactory = SplitSdk(sdkBrowser); let hookResult; render( @@ -26,6 +27,7 @@ describe('useSplitManager', () => { })} ); + expect(hookResult).toStrictEqual({ manager: outerFactory.manager(), client: outerFactory.client(), @@ -37,6 +39,20 @@ describe('useSplitManager', () => { isTimedout: false, lastUpdate: 0, }); + + act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); + + expect(hookResult).toStrictEqual({ + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, + hasTimedout: false, + isDestroyed: false, + isReady: true, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: getStatus(outerFactory.client()).lastUpdate, + }); }); test('returns null if invoked outside Split context.', () => { @@ -47,6 +63,7 @@ describe('useSplitManager', () => { return null; }) ); + expect(hookResult).toStrictEqual({ manager: null, client: null, From 75bddecc5f2b83990a90d41c31bc521f4ee99be8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 15:27:42 -0300 Subject: [PATCH 48/67] Update test --- src/__tests__/withSplitTreatments.test.tsx | 32 ++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index b7210c9..d1e665f 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -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 ; }); + render(); }); From af209f88448205c65bd8bf9e06cbab53fc691007 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 15:42:50 -0300 Subject: [PATCH 49/67] Update deprecation comment --- src/useClient.ts | 2 +- src/useTreatments.ts | 2 +- types/useClient.d.ts | 2 +- types/useTreatments.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/useClient.ts b/src/useClient.ts index 517dc81..7f41c58 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -8,7 +8,7 @@ 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 useSplitClient is the new hook to use. + * @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; diff --git a/src/useTreatments.ts b/src/useTreatments.ts index e6ba0e3..a794eb6 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -8,7 +8,7 @@ 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 useSplitTreatments is the new hook to use. + * @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; diff --git a/types/useClient.d.ts b/types/useClient.d.ts index 85f91c9..3369a1a 100644 --- a/types/useClient.d.ts +++ b/types/useClient.d.ts @@ -6,6 +6,6 @@ * @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 useSplitClient is the new hook to use. + * @deprecated Replace with the new `useSplitClient` hook. */ export declare function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts index d48695b..a5d25e1 100644 --- a/types/useTreatments.d.ts +++ b/types/useTreatments.d.ts @@ -6,6 +6,6 @@ * @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 useSplitTreatments is the new hook to use. + * @deprecated Replace with the new `useSplitTreatments` hook. */ export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; From 209a4125f6a8ccffbb1fa92cab2d16bd03f5f1c6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 16:00:18 -0300 Subject: [PATCH 50/67] Update doc comments --- src/useSplitClient.ts | 6 ++++++ src/useSplitTreatments.ts | 10 ++++++++-- types/useSplitClient.d.ts | 6 ++++++ types/useSplitTreatments.d.ts | 10 ++++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index cd3a11c..5e2e6a6 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -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 { diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index ddc0590..56fb0db 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -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 { diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts index 90868e4..b4dc1ea 100644 --- a/types/useSplitClient.d.ts +++ b/types/useSplitClient.d.ts @@ -10,6 +10,12 @@ export declare 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 declare function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues; diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts index b636632..1f19619 100644 --- a/types/useSplitTreatments.d.ts +++ b/types/useSplitTreatments.d.ts @@ -1,9 +1,15 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; /** - * '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 declare function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps; From 43f72134d272c8f51327068400917e128bf276ed Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 16:06:03 -0300 Subject: [PATCH 51/67] Update deprecation comment --- src/useManager.ts | 2 +- src/useSplitManager.ts | 6 ++++++ types/useManager.d.ts | 2 +- types/useSplitManager.d.ts | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/useManager.ts b/src/useManager.ts index a0383f3..b5771c2 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -8,7 +8,7 @@ import { useSplitManager } from './useSplitManager'; * @return A Split Manager instance, or null if used outside the scope of SplitFactory * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * - * @deprecated useSplitManager is the new hook to use. + * @deprecated Replace with the new `useSplitManager` hook. */ export function useManager(): SplitIO.IManager | null { return useSplitManager().manager; diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index df54a50..d6180b2 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -7,6 +7,12 @@ import { ISplitContextValues } from './types'; * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. * * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * + * @example + * ```js + * const { manager, isReady } = useSplitManager(); + * ``` + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} */ export function useSplitManager(): ISplitContextValues & { manager: SplitIO.IManager | null } { diff --git a/types/useManager.d.ts b/types/useManager.d.ts index 33ffbf2..02c5e11 100644 --- a/types/useManager.d.ts +++ b/types/useManager.d.ts @@ -6,6 +6,6 @@ * @return A Split Manager instance, or null if used outside the scope of SplitFactory * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * - * @deprecated useSplitManager is the new hook to use. + * @deprecated Replace with the new `useSplitManager` hook. */ export declare function useManager(): SplitIO.IManager | null; diff --git a/types/useSplitManager.d.ts b/types/useSplitManager.d.ts index a1c3d3e..7a2118d 100644 --- a/types/useSplitManager.d.ts +++ b/types/useSplitManager.d.ts @@ -4,6 +4,12 @@ import { ISplitContextValues } from './types'; * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. * * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * + * @example + * ```js + * const { manager, isReady } = useSplitManager(); + * ``` + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} */ export declare function useSplitManager(): ISplitContextValues & { From 91aeb65c42f3f27bd2f4cf6f83b268592beb633a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 16:21:06 -0300 Subject: [PATCH 52/67] changelog polishing --- CHANGES.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 905d4e0..2afe5f1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,14 +1,14 @@ 1.10.0 (November XX, 2023) - - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks to use instead of `useClient`, `useTreatments` and `useManager` respectively, which are deprecated now. - - The new hooks return the Split context object together with the SDK client, treatments and manager respectively, rather than returning only the client, treatments or manager 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. - - `useClient` and `useTreatments` 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 `useClient` and `useTreatments` 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). + - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks as replacements for the now deprecated `useClient`, `useTreatments` and `useManager` hooks. + - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. + - `useClient` and `useTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. + - `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize `getTreatmentsWithConfig` method calls from the SDK. 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 `useClient` and `useTreatments` hooks, which caused them to not re-render and re-evaluate feature flags when they access 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, e.g., `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 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 - Removed conditional code within hooks to adhere to the rules of hooks and prevent React warnings. 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). 1.9.0 (July 18, 2023) From 815b49b713ac222346cebf60f2247a05b5659a36 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 16:32:34 -0300 Subject: [PATCH 53/67] replace the 'split' term with 'feature flag' --- src/__tests__/SplitTreatments.test.tsx | 4 ++-- src/__tests__/useSplitTreatments.test.tsx | 4 ++-- src/types.ts | 4 ++-- src/utils.ts | 4 ++-- types/types.d.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index f3c2f56..29e641c 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -136,8 +136,8 @@ describe('SplitTreatments', () => { }} ); - 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(); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index f5000d6..168bf0e 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -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 () => { diff --git a/src/types.ts b/src/types.ts index 42d2a85..0a9ecdd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; diff --git a/src/utils.ts b/src/utils.ts index 5d48611..451582c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 @@ -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)) { diff --git a/types/types.d.ts b/types/types.d.ts index 0e4c275..d2913bc 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -157,8 +157,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; From 6fd7c7e0d5578cf790154fcee2de1fce65cf4152 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 17:10:45 -0300 Subject: [PATCH 54/67] Add changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 86cafe4..8ca12f3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,9 @@ 1.10.0 (September XX, 2023) + - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` argument to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. + - Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. + - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. - 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. From 3f24e2400cba7e4052fa87f8455e2d41ca3cc212 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 17:11:28 -0300 Subject: [PATCH 55/67] Update JS SDK version --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 455ce50..82f1b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.2-rc.3", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -1547,9 +1547,9 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.2-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", - "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "dependencies": { "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", @@ -12089,9 +12089,9 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.2-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", - "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "requires": { "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", diff --git a/package.json b/package.json index cb69ffc..0605b11 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.23.2-rc.3", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, From dadcdc68574d14d4371fd56bcdd0642ea340a093 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 11:11:11 -0300 Subject: [PATCH 56/67] Add 'types' folder to .gitignore, because it's generated by the build script --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 568cd2b..204ddd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ /lib /es /umd +/types /coverage .scannerwork From ecd1b0c824efd154c90923917da48a27724868d6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 11:19:44 -0300 Subject: [PATCH 57/67] Stop versioning 'types' folder. No need to track since it's auto-generated code --- types/SplitClient.d.ts | 64 ------------ types/SplitContext.d.ts | 9 -- types/SplitFactory.d.ts | 26 ----- types/SplitTreatments.d.ts | 14 --- types/constants.d.ts | 11 -- types/index.d.ts | 15 --- types/types.d.ts | 183 --------------------------------- types/useClient.d.ts | 11 -- types/useManager.d.ts | 9 -- types/useSplitClient.d.ts | 21 ---- types/useSplitTreatments.d.ts | 15 --- types/useTrack.d.ts | 8 -- types/useTreatments.d.ts | 11 -- types/utils.d.ts | 38 ------- types/withSplitClient.d.ts | 11 -- types/withSplitFactory.d.ts | 11 -- types/withSplitTreatments.d.ts | 11 -- 17 files changed, 468 deletions(-) delete mode 100644 types/SplitClient.d.ts delete mode 100644 types/SplitContext.d.ts delete mode 100644 types/SplitFactory.d.ts delete mode 100644 types/SplitTreatments.d.ts delete mode 100644 types/constants.d.ts delete mode 100644 types/index.d.ts delete mode 100644 types/types.d.ts delete mode 100644 types/useClient.d.ts delete mode 100644 types/useManager.d.ts delete mode 100644 types/useSplitClient.d.ts delete mode 100644 types/useSplitTreatments.d.ts delete mode 100644 types/useTrack.d.ts delete mode 100644 types/useTreatments.d.ts delete mode 100644 types/utils.d.ts delete mode 100644 types/withSplitClient.d.ts delete mode 100644 types/withSplitFactory.d.ts delete mode 100644 types/withSplitTreatments.d.ts diff --git a/types/SplitClient.d.ts b/types/SplitClient.d.ts deleted file mode 100644 index eb11670..0000000 --- a/types/SplitClient.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; -/** - * Common component used to handle the status and events of a Split client passed as prop. - * Reused by both SplitFactory (main client) and SplitClient (shared client) components. - */ -export declare class SplitComponent extends React.Component { - static defaultProps: { - updateOnSdkUpdate: boolean; - updateOnSdkTimedout: boolean; - updateOnSdkReady: boolean; - updateOnSdkReadyFromCache: boolean; - children: null; - factory: null; - client: null; - }; - static getDerivedStateFromProps(props: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }, state: ISplitContextValues): { - isReady: boolean; - isReadyFromCache: boolean; - isTimedout: boolean; - hasTimedout: boolean; - isDestroyed: boolean; - lastUpdate: number; - client: import("@splitsoftware/splitio/types/splitio").IBrowserClient | null; - factory: import("@splitsoftware/splitio/types/splitio").IBrowserSDK | null; - } | null; - readonly state: Readonly; - constructor(props: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }); - subscribeToEvents(client: SplitIO.IBrowserClient | null): void; - unsubscribeFromEvents(client: SplitIO.IBrowserClient | null): void; - setReady: () => void; - setReadyFromCache: () => void; - setTimedout: () => void; - setUpdate: () => void; - componentDidMount(): void; - componentDidUpdate(prevProps: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }): void; - componentWillUnmount(): void; - render(): JSX.Element; -} -/** - * SplitClient will initialize a new SDK client and listen for its events in order to update the Split Context. - * Children components will have access to the new client when accessing Split Context. - * - * Unlike SplitFactory, the underlying SDK client can be changed during the component lifecycle - * if the component is updated with a different splitKey or trafficType prop. Since the client can change, - * its release is not handled by SplitClient but by its container SplitFactory component. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} - */ -export declare function SplitClient(props: ISplitClientProps): JSX.Element; diff --git a/types/SplitContext.d.ts b/types/SplitContext.d.ts deleted file mode 100644 index def9f03..0000000 --- a/types/SplitContext.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { ISplitContextValues } from './types'; -export declare const INITIAL_CONTEXT: ISplitContextValues; -/** - * Split Context is the React Context instance that represents our SplitIO global state. - * It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate) - * The context is created with default empty values, that eventually SplitFactory and SplitClient access and update. - */ -export declare const SplitContext: React.Context; diff --git a/types/SplitFactory.d.ts b/types/SplitFactory.d.ts deleted file mode 100644 index 34f8d5a..0000000 --- a/types/SplitFactory.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { ISplitFactoryProps } from './types'; -/** - * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, - * and automatically shutdown and release resources when it is unmounted. SplitFactory must wrap other components and functions - * from this library, since they access the Split Context and its elements (factory, clients, etc). - * - * The underlying SDK factory and client is set on the constructor, and cannot be changed during the component lifecycle, - * even if the component is updated with a different config or factory prop. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK} - */ -export declare class SplitFactory extends React.Component { - static defaultProps: ISplitFactoryProps; - readonly state: Readonly<{ - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }>; - readonly isFactoryExternal: boolean; - constructor(props: ISplitFactoryProps); - componentWillUnmount(): void; - render(): JSX.Element; -} diff --git a/types/SplitTreatments.d.ts b/types/SplitTreatments.d.ts deleted file mode 100644 index 70e213a..0000000 --- a/types/SplitTreatments.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { ISplitTreatmentsProps } from './types'; -/** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} - */ -export declare class SplitTreatments extends React.Component { - private logWarning?; - private evaluateFeatureFlags; - render(): JSX.Element; - componentDidMount(): void; -} diff --git a/types/constants.d.ts b/types/constants.d.ts deleted file mode 100644 index e382449..0000000 --- a/types/constants.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export declare const VERSION: string; -export declare const ON: SplitIO.Treatment; -export declare const OFF: SplitIO.Treatment; -export declare const CONTROL: SplitIO.Treatment; -export declare const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig; -export declare const getControlTreatmentsWithConfig: (featureFlagNames: unknown) => SplitIO.TreatmentsWithConfig; -export declare const WARN_SF_CONFIG_AND_FACTORY: string; -export declare const ERROR_SF_NO_CONFIG_AND_FACTORY: string; -export declare const ERROR_SC_NO_FACTORY: string; -export declare const WARN_ST_NO_CLIENT: string; -export declare const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string; diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 76e03f3..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -export { withSplitFactory } from './withSplitFactory'; -export { withSplitClient } from './withSplitClient'; -export { withSplitTreatments } from './withSplitTreatments'; -export { SplitTreatments } from './SplitTreatments'; -export { SplitClient } from './SplitClient'; -export { SplitFactory } from './SplitFactory'; -export { useClient } from './useClient'; -export { useTreatments } from './useTreatments'; -export { useTrack } from './useTrack'; -export { useManager } from './useManager'; -export { useSplitClient } from './useSplitClient'; -export { useSplitTreatments } from './useSplitTreatments'; -export { SplitContext } from './SplitContext'; -export type { ISplitClientChildProps, ISplitClientProps, ISplitContextValues, ISplitFactoryChildProps, ISplitFactoryProps, ISplitStatus, ISplitTreatmentsChildProps, ISplitTreatmentsProps, IUpdateProps } from './types'; diff --git a/types/types.d.ts b/types/types.d.ts deleted file mode 100644 index d2913bc..0000000 --- a/types/types.d.ts +++ /dev/null @@ -1,183 +0,0 @@ -import SplitIO from '@splitsoftware/splitio/types/splitio'; -import type { ReactNode } from 'react'; -/** - * Split Status interface. It represents the current readiness state of the SDK. - */ -export interface ISplitStatus { - /** - * isReady indicates if the Split SDK client has triggered an SDK_READY event and thus is ready to be consumed. - */ - isReady: boolean; - /** - * isReadyFromCache indicates if the Split SDK client has triggered an SDK_READY_FROM_CACHE event and thus is ready to be consumed, - * although the data in cache might be stale. - */ - isReadyFromCache: boolean; - /** - * isTimedout indicates if the Split SDK client has triggered an SDK_READY_TIMED_OUT event and is not ready to be consumed. - */ - isTimedout: boolean; - /** - * hasTimedout indicates if the Split SDK client has ever triggered an SDK_READY_TIMED_OUT event. - * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. - */ - hasTimedout: boolean; - /** - * isDestroyed indicates if the Split SDK client has been destroyed. - */ - isDestroyed: boolean; - /** - * Indicates when was the last status event, either SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT or SDK_UPDATE. - */ - 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 - */ - factory: SplitIO.IBrowserSDK | null; - /** - * Split client instance - * @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 | null; -} -/** - * Update Props interface. It defines the props used to configure what SDK events are listened to update the Split context. - * Only `SDK_UPDATE` and `SDK_READY_TIMED_OUT` are configurable. - * The `SDK_READY` event is always listened to update the Split context value 'isReady'. - */ -export interface IUpdateProps { - /** - * updateOnSdkUpdate indicates if the component will update the `SplitContext` in case of a `SDK_UPDATE` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_UPDATE. - * It's value is false by default. - */ - updateOnSdkUpdate?: boolean; - /** - * updateOnSdkTimedout indicates if the component will update the `SplitContext` 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. - * It's value is false by default. - */ - updateOnSdkTimedout?: boolean; - /** - * updateOnSdkReady indicates if the component will update the `SplitContext` in case of a `SDK_READY` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY. - * It's value is true by default. - */ - updateOnSdkReady?: boolean; - /** - * updateOnSdkReadyFromCache indicates if the component will update the `SplitContext` 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. - * 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; -} -/** - * SplitFactory Child Props interface. These are the props that the child component receives from the 'SplitFactory' component. - */ -export interface ISplitFactoryChildProps extends ISplitContextValues { -} -/** - * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. - */ -export interface ISplitFactoryProps extends IUpdateProps { - /** - * Config object used to instantiate a Split factory - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration} - */ - config?: SplitIO.IBrowserSettings; - /** - * Split factory instance to use instead of creating a new one with the config object. - */ - factory?: SplitIO.IBrowserSDK; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; - /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. - */ - children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode; -} -/** - * useSplitClient options interface. This is the options object accepted by useSplitClient hook, - * used to retrieve a client instance with the Split context, and listen for SDK events. - */ -export interface IUseSplitClientOptions extends IUpdateProps { - /** - * The customer identifier. - */ - splitKey?: SplitIO.SplitKey; - /** - * Traffic type associated with the customer identifier. - * If no provided here or at the config object, it will be required on the client.track() calls. - */ - trafficType?: string; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; -} -/** - * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. - */ -export interface ISplitClientChildProps extends ISplitContextValues { -} -/** - * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instance, update the Split context, and listen for SDK events. - */ -export interface ISplitClientProps extends IUseSplitClientOptions { - /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. - */ - children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; -} -/** - * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. - */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { - /** - * list of feature flag names - */ - names: string[]; -} -/** - * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. - */ -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: - * { - * feature1: { treatment: 'on', config: null } - * feature2: { treatment: 'off', config: '{"bannerText":"Click here."}' } - * } - */ - treatments: SplitIO.TreatmentsWithConfig; -} -/** - * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, - * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. - */ -export interface ISplitTreatmentsProps { - /** - * list of feature flag names - */ - names: string[]; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; - /** - * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. - */ - children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} diff --git a/types/useClient.d.ts b/types/useClient.d.ts deleted file mode 100644 index 3369a1a..0000000 --- a/types/useClient.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 'useClient' is a hook that returns a client from the Split context. - * 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 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 declare function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; diff --git a/types/useManager.d.ts b/types/useManager.d.ts deleted file mode 100644 index 68257aa..0000000 --- a/types/useManager.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 'useManager' is a hook that returns the Manager instance from the Split factory. - * It uses the 'useContext' hook to access the factory at Split context, which is updated by - * the SplitFactory component. - * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} - */ -export declare function useManager(): SplitIO.IManager | null; diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts deleted file mode 100644 index b4dc1ea..0000000 --- a/types/useSplitClient.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ISplitContextValues, IUseSplitClientOptions } from './types'; -export declare const DEFAULT_UPDATE_OPTIONS: { - updateOnSdkUpdate: boolean; - updateOnSdkTimedout: boolean; - updateOnSdkReady: boolean; - updateOnSdkReadyFromCache: boolean; -}; -/** - * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. - * 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 declare function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues; diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts deleted file mode 100644 index 1f19619..0000000 --- a/types/useSplitTreatments.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; -/** - * '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 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 declare function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps; diff --git a/types/useTrack.d.ts b/types/useTrack.d.ts deleted file mode 100644 index ff25094..0000000 --- a/types/useTrack.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * 'useTrack' is a hook that returns the track method from a Split client. - * It uses the 'useContext' hook to access the client from the Split context. - * - * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} - */ -export declare function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track']; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts deleted file mode 100644 index a5d25e1..0000000 --- a/types/useTreatments.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 'useTreatments' is a hook that returns an object of feature flag evaluations (i.e., treatments). - * It uses the 'useContext' hook to access the client from the Split context, - * and invokes the 'getTreatmentsWithConfig' method. - * - * @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 declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; diff --git a/types/utils.d.ts b/types/utils.d.ts deleted file mode 100644 index 942eb0d..0000000 --- a/types/utils.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ISplitStatus } from './types'; -/** - * ClientWithContext interface. - */ -export interface IClientWithContext extends SplitIO.IBrowserClient { - __getStatus(): { - isReady: boolean; - isReadyFromCache: boolean; - isOperational: boolean; - hasTimedout: boolean; - isDestroyed: boolean; - }; - lastUpdate: number; -} -/** - * FactoryWithClientInstances interface. - */ -export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - clientInstances: Set; - config: SplitIO.IBrowserSettings; -} -export declare const __factories: Map; -export declare function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients; -export declare function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext; -export declare function destroySplitFactory(factory: IFactoryWithClients): Promise; -export declare function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus; -export declare function validateFeatureFlags(maybeFeatureFlags: unknown, listName?: string): false | string[]; -/** - * Manage client attributes binding - */ -export declare function initAttributes(client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes): void; -/** - * Gets a memoized version of the `client.getTreatmentsWithConfig` method. - * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. - */ -export declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; -declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; -export {}; diff --git a/types/withSplitClient.d.ts b/types/withSplitClient.d.ts deleted file mode 100644 index 5a98bcf..0000000 --- a/types/withSplitClient.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitClientChildProps } from './types'; -/** - * High-Order Component for SplitClient. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitClient (see ISplitClientChildProps). - * - * @param splitKey The customer identifier. - * @param trafficType Traffic type associated with the customer identifier. If no provided here or at the config object, it will be required on the client.track() calls. - */ -export declare function withSplitClient(splitKey: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType, updateOnSdkUpdate?: boolean, updateOnSdkTimedout?: boolean, updateOnSdkReady?: boolean, updateOnSdkReadyFromCache?: boolean) => (props: OuterProps) => JSX.Element; diff --git a/types/withSplitFactory.d.ts b/types/withSplitFactory.d.ts deleted file mode 100644 index 183ac37..0000000 --- a/types/withSplitFactory.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitFactoryChildProps } from './types'; -/** - * High-Order Component for SplitFactory. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitFactory (see ISplitFactoryChildProps). - * - * @param config Config object used to instantiate a Split factory - * @param factory Split factory instance to use instead of creating a new one with the config object. - */ -export declare function withSplitFactory(config?: SplitIO.IBrowserSettings, factory?: SplitIO.IBrowserSDK, attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType, updateOnSdkUpdate?: boolean, updateOnSdkTimedout?: boolean, updateOnSdkReady?: boolean, updateOnSdkReadyFromCache?: boolean) => (props: OuterProps) => JSX.Element; diff --git a/types/withSplitTreatments.d.ts b/types/withSplitTreatments.d.ts deleted file mode 100644 index 848c42e..0000000 --- a/types/withSplitTreatments.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitTreatmentsChildProps } from './types'; -/** - * High-Order Component for SplitTreatments. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitTreatments (see ISplitTreatmentsChildProps). - * - * @param names list of feature flag names - * @param attributes An object of type Attributes used to evaluate the feature flags. - */ -export declare function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType) => (props: OuterProps) => JSX.Element; From 902c38bc926b84ab3c403f97d8f6fb9d892416d5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 11:57:29 -0300 Subject: [PATCH 58/67] polishing --- CHANGES.txt | 3 +-- src/SplitTreatments.tsx | 4 ++-- src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/useSplitTreatments.test.tsx | 2 +- src/constants.ts | 2 +- src/types.ts | 8 ++++---- src/useSplitTreatments.ts | 8 ++++---- src/utils.ts | 2 +- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 16cf217..666fbc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,6 @@ 1.10.0 (November XX, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` argument to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. - - Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. + - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. - Added new `useSplitClient` and `useSplitTreatments` hooks to use instead of `useClient` and `useTreatments` respectively, which are deprecated now. diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index fd89a7a..995f108 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -6,8 +6,8 @@ import { memoizeGetTreatmentsWithConfig } from './utils'; /** * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to - * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method - * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. + * 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. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 87fdacf..605d0a3 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -160,7 +160,7 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 6e88c36..47dc3c7 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -252,7 +252,7 @@ describe('useSplitTreatments', () => { }) ); - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); }); }); diff --git a/src/constants.ts b/src/constants.ts index c5675d0..daa2ba9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,4 +40,4 @@ export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; -export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'; +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets props were provided. flagSets will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 9bbc96d..cdce977 100644 --- a/src/types.ts +++ b/src/types.ts @@ -172,9 +172,9 @@ export interface ISplitClientProps extends IUseSplitClientOptions { /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { +export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names @@ -205,9 +205,9 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { /** * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, - * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export type ISplitTreatmentsProps = { +export interface ISplitTreatmentsProps { /** * An object of type Attributes used to evaluate the feature flags. diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 59fb776..309a6a2 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -5,8 +5,8 @@ import { useSplitClient } from './useSplitClient'; /** * '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 if `names` option is provided, - * or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` option is provided. + * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, + * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. * * @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. * @@ -24,8 +24,8 @@ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitT const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); - // Clone `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. - // Note: the same issue occurs with `names` and `attributes` arguments if the user mutates them directly instead of providing a new object. + // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked. + // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object. const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); return { diff --git a/src/utils.ts b/src/utils.ts index 82bf7d7..ee42523 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -189,5 +189,5 @@ function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes) : names ? getControlTreatmentsWithConfig(names) : - {} + {} // empty object when evaluating with flag sets and client is not ready } From 9ea53a093d81f8f9c70f8c268a8f66e46325656d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 12:13:51 -0300 Subject: [PATCH 59/67] Update type definition comment for names and flagSets params --- src/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index cdce977..4179e93 100644 --- a/src/types.ts +++ b/src/types.ts @@ -177,12 +177,12 @@ export interface ISplitClientProps extends IUseSplitClientOptions { export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** - * list of feature flag names + * list of feature flag names to evaluate. If provided, the `flagSets` option is ignored. */ names?: string[]; /** - * list of feature flag sets + * list of feature flag sets to evaluate. */ flagSets?: string[]; } @@ -220,12 +220,12 @@ export interface ISplitTreatmentsProps { children: ((props: ISplitTreatmentsChildProps) => ReactNode); /** - * list of feature flag names + * list of feature flag names to evaluate. If provided, the `flagSets` prop is ignored. */ names?: string[]; /** - * list of feature flag sets + * list of feature flag sets to evaluate. */ flagSets?: string[]; } From 2a59f3652f9225d053f4c8df622f85a019ecdd55 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:45:45 -0300 Subject: [PATCH 60/67] Reuse GetTreatmentsOptions type --- src/types.ts | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4179e93..ae43b2c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,23 +170,30 @@ export interface ISplitClientProps extends IUseSplitClientOptions { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } -/** - * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. - */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export type GetTreatmentsOptions = { /** - * list of feature flag names to evaluate. If provided, the `flagSets` option is ignored. + * List of feature flag names to evaluate. Either this or the `flagSets` property must be provided. If both are provided, the `flagSets` option is ignored. */ names?: string[]; /** - * list of feature flag sets to evaluate. + * List of feature flag sets to evaluate. Either this or the `names` property must be provided. If both are provided, the `flagSets` option is ignored. */ flagSets?: string[]; + + /** + * An object of type Attributes used to evaluate the feature flags. + */ + attributes?: SplitIO.Attributes; } +/** + * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. + */ +export interface IUseSplitTreatmentsOptions extends GetTreatmentsOptions, IUseSplitClientOptions { } + /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -207,25 +214,10 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export interface ISplitTreatmentsProps { - - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; +export interface ISplitTreatmentsProps extends GetTreatmentsOptions { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); - - /** - * list of feature flag names to evaluate. If provided, the `flagSets` prop is ignored. - */ - names?: string[]; - - /** - * list of feature flag sets to evaluate. - */ - flagSets?: string[]; } From accf9d26b106b68c908c52abacf7b87b32b71093 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:53:24 -0300 Subject: [PATCH 61/67] Specialize the GetTreatmentsOptions type to mutually exclude names and flagSets properties --- src/__tests__/SplitTreatments.test.tsx | 3 +++ src/__tests__/useSplitTreatments.test.tsx | 4 ++++ src/types.ts | 14 +++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 605d0a3..69602fd 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -152,6 +152,7 @@ describe('SplitTreatments', () => { test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { render( + // @ts-expect-error flagSets and names are mutually exclusive {({ treatments }) => { expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); @@ -171,6 +172,7 @@ let renderTimes = 0; */ describe.each([ ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => ( + // @ts-expect-error names and flagSets are mutually exclusive {() => { renderTimes++; @@ -179,6 +181,7 @@ describe.each([ ), ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => { + // @ts-expect-error names and flagSets are mutually exclusive useSplitTreatments({ names, flagSets, attributes }); renderTimes++; return null; diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 47dc3c7..3364a87 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -37,6 +37,9 @@ describe('useSplitTreatments', () => { {React.createElement(() => { treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; + + // @ts-expect-error Options object must provide either names or flagSets + expect(useSplitTreatments({}).treatments).toEqual({}); return null; })} @@ -246,6 +249,7 @@ describe('useSplitTreatments', () => { test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { render( React.createElement(() => { + // @ts-expect-error names and flagSets are mutually exclusive const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); return null; diff --git a/src/types.ts b/src/types.ts index ae43b2c..81b735e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,17 +170,21 @@ export interface ISplitClientProps extends IUseSplitClientOptions { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } -export type GetTreatmentsOptions = { +export type GetTreatmentsOptions = ({ /** * List of feature flag names to evaluate. Either this or the `flagSets` property must be provided. If both are provided, the `flagSets` option is ignored. */ - names?: string[]; + names: string[]; + flagSets?: undefined; +} | { /** * List of feature flag sets to evaluate. Either this or the `names` property must be provided. If both are provided, the `flagSets` option is ignored. */ - flagSets?: string[]; + flagSets: string[]; + names?: undefined; +}) & { /** * An object of type Attributes used to evaluate the feature flags. @@ -192,7 +196,7 @@ export type GetTreatmentsOptions = { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends GetTreatmentsOptions, IUseSplitClientOptions { } +export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions; /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -214,7 +218,7 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export interface ISplitTreatmentsProps extends GetTreatmentsOptions { +export type ISplitTreatmentsProps = GetTreatmentsOptions & { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. From 44c4b90081b9fa6bd074a1512424559a5271a49f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:57:19 -0300 Subject: [PATCH 62/67] prepare rc --- .github/workflows/ci.yml | 4 ++-- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3a8b1..4262bf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/flagSets_xor') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/development' + if: github.event_name == 'push' && github.ref == 'refs/heads/flagSets_xor' strategy: matrix: environment: diff --git a/CHANGES.txt b/CHANGES.txt index 6e62c68..e3a50d3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.10.0 (November XX, 2023) +1.10.0 (November 16, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. diff --git a/package-lock.json b/package-lock.json index b112dcf..b454bd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.24.0-beta", diff --git a/package.json b/package.json index 59d2a09..3aad27b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", From a46a180d9498a5df2a3c7dbc7be073c27b9c93c5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:20:05 -0300 Subject: [PATCH 63/67] polishing --- .github/workflows/ci.yml | 4 ++-- src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/useSplitTreatments.test.tsx | 2 +- src/constants.ts | 2 +- src/useClient.ts | 3 ++- src/useManager.ts | 3 ++- src/useSplitClient.ts | 2 +- src/useSplitManager.ts | 2 +- src/useSplitTreatments.ts | 2 +- src/useTrack.ts | 3 ++- src/useTreatments.ts | 3 ++- 11 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4262bf8..bc3a8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/flagSets_xor') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/flagSets_xor' + if: github.event_name == 'push' && github.ref == 'refs/heads/development' strategy: matrix: environment: diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 69602fd..13d8990 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -161,7 +161,7 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); + expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 3364a87..cbdc62d 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -256,7 +256,7 @@ describe('useSplitTreatments', () => { }) ); - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/constants.ts b/src/constants.ts index daa2ba9..47020ef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,4 +40,4 @@ export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; -export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets props were provided. flagSets will be ignored.'; +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/useClient.ts b/src/useClient.ts index 7f41c58..535eb98 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -5,7 +5,8 @@ import { useSplitClient } from './useSplitClient'; * 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 Client instance, or null if used outside the scope of SplitFactory + * @returns 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. diff --git a/src/useManager.ts b/src/useManager.ts index b5771c2..9ba101b 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -5,7 +5,8 @@ import { useSplitManager } from './useSplitManager'; * It uses the 'useContext' hook to access the factory at Split context, which is updated by * the SplitFactory component. * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory + * @returns A Split Manager instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * * @deprecated Replace with the new `useSplitManager` hook. diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5e2e6a6..a0c696c 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -14,7 +14,7 @@ export const DEFAULT_UPDATE_OPTIONS = { * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * 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 + * @returns A Split Context object * * @example * ```js diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index d6180b2..1ba6a23 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -6,7 +6,7 @@ import { ISplitContextValues } from './types'; * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. * - * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory * * @example * ```js diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 309a6a2..d5dbf6d 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -8,7 +8,7 @@ import { useSplitClient } from './useSplitClient'; * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. * - * @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. + * @returns 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 diff --git a/src/useTrack.ts b/src/useTrack.ts index 108885e..b511018 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -7,7 +7,8 @@ const noOpFalse = () => false; * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * @returns A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { diff --git a/src/useTreatments.ts b/src/useTreatments.ts index a794eb6..bd673fa 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -5,7 +5,8 @@ import { useSplitTreatments } from './useSplitTreatments'; * It uses the 'useContext' hook to access the client from the Split context, * and invokes the 'getTreatmentsWithConfig' method. * - * @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. + * @returns 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. From 9d0175194b274969e742abec44305740cd407b09 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:33:23 -0300 Subject: [PATCH 64/67] Fix indentation in CHANGES.txt file --- CHANGES.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2afe5f1..bd0383b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ 1.10.0 (November XX, 2023) - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks as replacements for the now deprecated `useClient`, `useTreatments` and `useManager` hooks. - - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. - - `useClient` and `useTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. + - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. + - `useSplitClient` and `useSplitTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. - `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize `getTreatmentsWithConfig` method calls from the SDK. 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 `useClient` and `useTreatments` hooks, which caused them to not re-render and re-evaluate feature flags when they access 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, e.g., `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react'` (Related to issue https://github.com/splitio/react-client/issues/162). From c42fd8c21ebd3c198cfa8ddb3f6939035c744d8e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:37:10 -0300 Subject: [PATCH 65/67] Replace @return with @returns, which is the correct JSDoc tag --- src/useClient.ts | 3 ++- src/useManager.ts | 3 ++- src/useSplitClient.ts | 2 +- src/useSplitManager.ts | 2 +- src/useSplitTreatments.ts | 2 +- src/useTrack.ts | 3 ++- src/useTreatments.ts | 3 ++- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/useClient.ts b/src/useClient.ts index 7f41c58..535eb98 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -5,7 +5,8 @@ import { useSplitClient } from './useSplitClient'; * 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 Client instance, or null if used outside the scope of SplitFactory + * @returns 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. diff --git a/src/useManager.ts b/src/useManager.ts index b5771c2..9ba101b 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -5,7 +5,8 @@ import { useSplitManager } from './useSplitManager'; * It uses the 'useContext' hook to access the factory at Split context, which is updated by * the SplitFactory component. * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory + * @returns A Split Manager instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * * @deprecated Replace with the new `useSplitManager` hook. diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5e2e6a6..a0c696c 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -14,7 +14,7 @@ export const DEFAULT_UPDATE_OPTIONS = { * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * 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 + * @returns A Split Context object * * @example * ```js diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index d6180b2..1ba6a23 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -6,7 +6,7 @@ import { ISplitContextValues } from './types'; * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. * - * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory * * @example * ```js diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 56fb0db..0d68615 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -8,7 +8,7 @@ import { useSplitClient } from './useSplitClient'; * '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 feature flag names do not exist. + * @returns 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 diff --git a/src/useTrack.ts b/src/useTrack.ts index 108885e..b511018 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -7,7 +7,8 @@ const noOpFalse = () => false; * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * @returns A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { diff --git a/src/useTreatments.ts b/src/useTreatments.ts index a794eb6..bd673fa 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -5,7 +5,8 @@ import { useSplitTreatments } from './useSplitTreatments'; * It uses the 'useContext' hook to access the client from the Split context, * and invokes the 'getTreatmentsWithConfig' method. * - * @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. + * @returns 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. From fab73a0cd87fccea7a421b79e1c7dc9263184b1d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:55:31 -0300 Subject: [PATCH 66/67] prepare stable version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b454bd5..f1c9717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.24.0-beta", diff --git a/package.json b/package.json index 3aad27b..4a497b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", From 263ee5cee6f7e2e5d7bd432dd1c46c704dafd30f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 16:05:55 -0300 Subject: [PATCH 67/67] update changes entry --- CHANGES.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8c012a3..053cfd6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,15 +2,14 @@ - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. + - Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views. - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks as replacements for the now deprecated `useClient`, `useTreatments` and `useManager` hooks. - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. - `useSplitClient` and `useSplitTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. - `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize `getTreatmentsWithConfig` method calls from the SDK. 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 `useClient` and `useTreatments` hooks, which caused them to not re-render and re-evaluate feature flags when they access 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). + - Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index, e.g., `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 evaluations. - Updated linter and other dependencies for vulnerability fixes. - Bugfixing - Removed conditional code within hooks to adhere to the rules of hooks and prevent React warnings. 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).