diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-cd.yml similarity index 98% rename from .github/workflows/ci.yml rename to .github/workflows/ci-cd.yml index bc3a8b1..3870a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-cd.yml @@ -23,14 +23,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up nodejs uses: actions/setup-node@v3 with: - node-version: '16.16.0' + node-version: 'lts/*' cache: 'npm' - name: npm ci diff --git a/.nvmrc b/.nvmrc index f274881..b009dfb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.16.0 +lts/* diff --git a/CHANGES.txt b/CHANGES.txt index 7f32a73..dd1026b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +1.11.0 (January 15, 2023) + - Added new `SplitFactoryProvider` component as replacement for the now deprecated `SplitFactory` component. + This new component is a fixed version of the `SplitFactory` component, which is not handling the SDK initialization side-effects in the `componentDidMount` and `componentDidUpdate` methods (commit phase), causing some issues like the SDK not being reinitialized when component props change (Related to issue #11 and #148). + The new component also supports server-side rendering. See our documentation for more details: https://help.split.io/hc/en-us/articles/360038825091-React-SDK#server-side-rendering (Related to issue #11 and #109). + - Updated internal code to remove a circular dependency and avoid warning messages with tools like PNPM (Related to issue #176). + 1.10.2 (December 12, 2023) - Updated @splitsoftware/splitio package to version 10.24.1 that updates localStorage usage to clear cached feature flag definitions before initiating the synchronization process, if the cache was previously synchronized with a different SDK key (i.e., a different environment) or different Split Filter criteria, to avoid using invalid cached data when the SDK is ready from cache. diff --git a/README.md b/README.md index a171ed1..53c4b7f 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, useSplitTreatments } from '@splitsoftware/splitio-react'; +import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react'; // Define your config object const CONFIG = { @@ -48,10 +48,10 @@ function MyComponent() { function MyApp() { return ( - // Use SplitFactory to instantiate the SDK and makes it available to nested components - + // Use SplitFactoryProvider to instantiate the SDK and makes it available to nested components + - + ); } ``` diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 0e735a8..fa584f4 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -1,13 +1,12 @@ 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, 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. - * Reused by both SplitFactory (main client) and SplitClient (shared client) components. + * Reused by both SplitFactoryProvider (main client) and SplitClient (any client) components. */ export class SplitComponent extends React.Component { @@ -47,11 +46,6 @@ export class SplitComponent extends React.Component(INITIAL_CONTEXT); diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx index d3ac78b..4ecfd3c 100644 --- a/src/SplitFactoryProvider.tsx +++ b/src/SplitFactoryProvider.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SplitComponent } from './SplitClient'; import { ISplitFactoryProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus } from './utils'; +import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus, __factories } from './utils'; import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; /** @@ -27,24 +27,39 @@ export function SplitFactoryProvider(props: ISplitFactoryProps) { config = undefined; } - const [stateFactory, setStateFactory] = React.useState(propFactory || null); - const factory = propFactory || stateFactory; + const [configFactory, setConfigFactory] = React.useState(null); + const factory = propFactory || (configFactory && config === configFactory.config ? configFactory : null); const client = factory ? getSplitClient(factory) : null; + // Effect to initialize and destroy the factory React.useEffect(() => { if (config) { const factory = getSplitFactory(config); + + return () => { + destroySplitFactory(factory); + } + } + }, [config]); + + // Effect to subscribe/unsubscribe to events + React.useEffect(() => { + const factory = config && __factories.get(config); + if (factory) { const client = getSplitClient(factory); const status = getStatus(client); - // Update state and unsubscribe from events when first event is emitted - const update = () => { + // Unsubscribe from events and update state when first event is emitted + const update = () => { // eslint-disable-next-line no-use-before-define + unsubscribe(); + setConfigFactory(factory); + } + + const unsubscribe = () => { 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); - - setStateFactory(factory); } if (updateOnSdkReady) { @@ -61,10 +76,7 @@ export function SplitFactoryProvider(props: ISplitFactoryProps) { } if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); - return () => { - // Factory destroy unsubscribes from events - destroySplitFactory(factory as IFactoryWithClients); - } + return unsubscribe; } }, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 995f108..da803c3 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitTreatmentsProps, ISplitContextValues } from './types'; -import { WARN_ST_NO_CLIENT } from './constants'; import { memoizeGetTreatmentsWithConfig } from './utils'; /** @@ -26,7 +25,7 @@ export class SplitTreatments extends React.Component { {(splitContext: ISplitContextValues) => { 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, @@ -35,9 +34,4 @@ export class SplitTreatments extends React.Component { ); } - - componentDidMount() { - if (this.logWarning) { console.log(WARN_ST_NO_CLIENT); } - } - } diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 482403e..3ab262c 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -11,17 +11,16 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ import { ISplitClientChildProps } from '../types'; -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; -import { ERROR_SC_NO_FACTORY } from '../constants'; import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('SplitClient', () => { test('passes no-ready props to the child if client is not ready.', () => { render( - + {({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { expect(isReady).toBe(false); @@ -34,7 +33,7 @@ describe('SplitClient', () => { return null; }} - + ); }); @@ -46,7 +45,7 @@ describe('SplitClient', () => { await outerFactory.client().ready(); render( - + {/* Equivalent to */} {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { @@ -61,7 +60,7 @@ describe('SplitClient', () => { return null; }} - + ); }); @@ -76,7 +75,7 @@ describe('SplitClient', () => { let previousLastUpdate = -1; render( - + {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -106,7 +105,7 @@ describe('SplitClient', () => { return null; }} - + ); act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); @@ -128,7 +127,7 @@ describe('SplitClient', () => { let previousLastUpdate = -1; render( - + {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -152,7 +151,7 @@ describe('SplitClient', () => { return null; }} - + ); act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); @@ -172,7 +171,7 @@ describe('SplitClient', () => { let previousLastUpdate = -1; render( - + {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -193,7 +192,7 @@ describe('SplitClient', () => { return null; }} - + ); act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); @@ -207,7 +206,7 @@ describe('SplitClient', () => { let count = 0; render( - + {({ client }) => { count++; @@ -221,7 +220,7 @@ describe('SplitClient', () => { return null; }} - + ); expect(count).toEqual(2); @@ -246,26 +245,27 @@ describe('SplitClient', () => { }; render( - + - + ); }); - test('logs error and passes null client if rendered outside an SplitProvider component.', () => { - const errorSpy = jest.spyOn(console, 'error'); - render( - - {({ client }) => { - expect(client).toBe(null); - return null; - }} - - ); - expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY); - }); + // @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy. + // test('logs error and passes null client if rendered outside an SplitProvider component.', () => { + // const errorSpy = jest.spyOn(console, 'error'); + // render( + // + // {({ client }) => { + // expect(client).toBe(null); + // return null; + // }} + // + // ); + // expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY); + // }); test(`passes a new client if re-rendered with a different splitKey. Only updates the state if the new client triggers an event, but not the previous one.`, (done) => { @@ -338,9 +338,9 @@ describe('SplitClient', () => { } render( - + - + ); }); @@ -348,14 +348,14 @@ describe('SplitClient', () => { function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( - + {() => { testSwitch(done, splitKey); return null; }} - + ); } diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index 2964a60..6745a4d 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -336,40 +336,95 @@ describe('SplitFactoryProvider', () => { logSpy.mockRestore(); }); - test('cleans up on unmount.', () => { - let destroyMainClientSpy; - let destroySharedClientSpy; - const wrapper = render( - - {({ factory }) => { - if (!factory) return null; // 1st render + test('cleans up on update and unmount if config prop is provided.', () => { + let renderTimes = 0; + const createdFactories = new Set(); + const clientDestroySpies: jest.SpyInstance[] = []; + const outerFactory = SplitSdk(sdkBrowser); + + const Component = ({ factory, isReady, hasTimedout }: ISplitFactoryChildProps) => { + renderTimes++; - // 2nd render (SDK ready) - expect(__factories.size).toBe(1); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); + switch (renderTimes) { + case 1: + expect(factory).toBe(outerFactory); + return null; + case 2: + case 5: + expect(isReady).toBe(false); + expect(hasTimedout).toBe(false); + expect(factory).toBe(null); + return null; + case 3: + case 4: + case 6: + expect(isReady).toBe(true); + expect(hasTimedout).toBe(true); + expect(factory).not.toBe(null); + createdFactories.add(factory!); + clientDestroySpies.push(jest.spyOn(factory!.client(), 'destroy')); return ( {({ client }) => { - destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); + clientDestroySpies.push(jest.spyOn(client!, 'destroy')); return null; }} ); - }} - - ); + case 7: + throw new Error('Must not rerender'); + } + }; - // SDK ready to re-render - act(() => { + const emitSdkEvents = () => { const factory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + factory.client().__emitter__.emit(Event.SDK_READY_TIMED_OUT) factory.client().__emitter__.emit(Event.SDK_READY) - }); + }; + + // 1st render: factory provided + const wrapper = render( + + {Component} + + ); + + // 2nd render: factory created, not ready (null) + wrapper.rerender( + + {Component} + + ); + + // 3rd render: SDK ready (timeout is ignored due to updateOnSdkTimedout=false) + act(emitSdkEvents); + + // 4th render: same config prop -> factory is not recreated + wrapper.rerender( + + {Component} + + ); + + act(emitSdkEvents); // Emitting events again has no effect + expect(createdFactories.size).toBe(1); + + // 5th render: Update config prop -> factory is recreated, not ready yet (null) + wrapper.rerender( + + {Component} + + ); + + // 6th render: SDK timeout (ready is ignored due to updateOnSdkReady=false) + act(emitSdkEvents); wrapper.unmount(); - // the factory created by the component is removed from `factories` cache and its clients are destroyed + + // Created factories are removed from `factories` cache and their clients are destroyed + expect(createdFactories.size).toBe(2); expect(__factories.size).toBe(0); - expect(destroyMainClientSpy).toBeCalledTimes(1); - expect(destroySharedClientSpy).toBeCalledTimes(1); + clientDestroySpies.forEach(spy => expect(spy).toBeCalledTimes(1)); }); test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => { @@ -381,11 +436,11 @@ describe('SplitFactoryProvider', () => { {({ factory }) => { // if factory is provided as a prop, `factories` cache is not modified expect(__factories.size).toBe(0); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); + destroyMainClientSpy = jest.spyOn(factory!.client(), 'destroy'); return ( {({ client }) => { - destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); + destroySharedClientSpy = jest.spyOn(client!, 'destroy'); return null; }} diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 13d8990..308379c 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -10,13 +10,13 @@ import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; import { getStatus } from '../utils'; import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; -import { CONTROL_WITH_CONFIG, WARN_ST_NO_CLIENT } from '../constants'; +import { CONTROL_WITH_CONFIG } from '../constants'; /** Test target */ import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types'; import { SplitTreatments } from '../SplitTreatments'; import { SplitClient } from '../SplitClient'; -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { useSplitTreatments } from '../useSplitTreatments'; const logSpy = jest.spyOn(console, 'log'); @@ -28,24 +28,20 @@ describe('SplitTreatments', () => { afterEach(() => { logSpy.mockClear() }); - it('passes control treatments (empty object if flagSets is provided) if the SDK is not ready.', () => { + it('passes control treatments (empty object if flagSets is provided) if the SDK is not operational.', () => { render( - - {({ factory }) => { + + {() => { return ( {({ treatments }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client('user1'); - 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; }} @@ -53,7 +49,7 @@ describe('SplitTreatments', () => { ); }} - + ); }); @@ -62,7 +58,7 @@ describe('SplitTreatments', () => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); render( - + {({ factory, isReady }) => { expect(getStatus(outerFactory.client()).isReady).toBe(isReady); expect(isReady).toBe(true); @@ -90,22 +86,23 @@ describe('SplitTreatments', () => { ); }} - + ); }); - it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { - render( - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); - return null; - }} - - ); + // @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy. + // it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { + // render( + // + // {({ treatments }: ISplitTreatmentsChildProps) => { + // expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + // return null; + // }} + // + // ); - expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); - }); + // expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); + // }); /** * Input validation. Passing invalid feature flag names or attributes while the Sdk @@ -113,7 +110,7 @@ describe('SplitTreatments', () => { */ it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { render( - + {() => { return ( <> @@ -141,7 +138,7 @@ describe('SplitTreatments', () => { ); }} - + ); 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.'); @@ -198,11 +195,11 @@ describe.each([ clientAttributes?: ISplitClientProps['attributes'] }) { return ( - + - + ); } @@ -302,27 +299,27 @@ describe.each([ expect(outerFactory.client('otherKey').getTreatmentsWithConfig).toBeCalledTimes(1); }); - it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => { + it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactoryProvider 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); let renderTimesComp1 = 0; let renderTimesComp2 = 0; - // test context updates on SplitFactory + // test context updates on SplitFactoryProvider render( - + {() => { renderTimesComp1++; return null; }} - + ); // test context updates on SplitClient render( - + {() => { @@ -331,7 +328,7 @@ describe.each([ }} - + ); expect(renderTimesComp1).toBe(1); diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index a4a32e8..29b894e 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -11,23 +11,23 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ import { useSplitClient } from '../useSplitClient'; -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('useSplitClient', () => { - test('returns the main client from the context updated by SplitFactory.', () => { + test('returns the main client from the context updated by SplitFactoryProvider.', () => { const outerFactory = SplitSdk(sdkBrowser); let client; render( - + {React.createElement(() => { client = useSplitClient().client; return null; })} - + ); expect(client).toBe(outerFactory.client()); }); @@ -36,14 +36,14 @@ describe('useSplitClient', () => { const outerFactory = SplitSdk(sdkBrowser); let client; render( - + {React.createElement(() => { client = useSplitClient().client; return null; })} - + ); expect(client).toBe(outerFactory.client('user2')); }); @@ -52,13 +52,13 @@ describe('useSplitClient', () => { const outerFactory = SplitSdk(sdkBrowser); let client; render( - + {React.createElement(() => { (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); @@ -89,9 +89,9 @@ describe('useSplitClient', () => { function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( - + - + ); } @@ -108,13 +108,13 @@ describe('useSplitClient', () => { let countNestedComponent = 0; render( - + <> {() => countSplitContext++} {() => { countSplitClient++; return null }} @@ -122,7 +122,7 @@ describe('useSplitClient', () => { {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 });` + // - Disabling update props, since the wrapping SplitFactoryProvider 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. @@ -183,7 +183,7 @@ describe('useSplitClient', () => { })} - + ); act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); @@ -226,7 +226,7 @@ describe('useSplitClient', () => { let count = 0; render( - + {React.createElement(() => { useSplitClient({ splitKey: 'some_user' }); count++; @@ -237,7 +237,7 @@ describe('useSplitClient', () => { return null; })} - + ) expect(count).toEqual(2); @@ -257,9 +257,9 @@ describe('useSplitClient', () => { function Component(updateOptions) { return ( - + - + ) } diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index 9ec5367..4705a48 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -11,7 +11,7 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; import { getStatus } from '../utils'; /** Test target */ -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { useSplitManager } from '../useSplitManager'; describe('useSplitManager', () => { @@ -20,12 +20,12 @@ describe('useSplitManager', () => { const outerFactory = SplitSdk(sdkBrowser); let hookResult; render( - + {React.createElement(() => { hookResult = useSplitManager(); return null; })} - + ); expect(hookResult).toStrictEqual({ diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index cbdc62d..13a54b6 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -11,7 +11,7 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; import { CONTROL_WITH_CONFIG } from '../constants'; /** Test target */ -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { useSplitTreatments } from '../useSplitTreatments'; import { SplitTreatments } from '../SplitTreatments'; @@ -33,7 +33,7 @@ describe('useSplitTreatments', () => { let treatmentsByFlagSets: SplitIO.TreatmentsWithConfig; render( - + {React.createElement(() => { treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; @@ -42,7 +42,7 @@ describe('useSplitTreatments', () => { expect(useSplitTreatments({}).treatments).toEqual({}); return null; })} - + ); // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method @@ -69,14 +69,14 @@ describe('useSplitTreatments', () => { 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 @@ -96,7 +96,7 @@ describe('useSplitTreatments', () => { let renderTimes = 0; render( - + {React.createElement(() => { const treatments = useSplitTreatments({ names: featureFlagNames, attributes, splitKey: 'user2' }).treatments; @@ -119,7 +119,7 @@ describe('useSplitTreatments', () => { return null; })} - + ); act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); @@ -189,7 +189,7 @@ describe('useSplitTreatments', () => { } render( - + <> {() => countSplitContext++} @@ -219,7 +219,7 @@ describe('useSplitTreatments', () => { return null; })} - + ); act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); diff --git a/src/__tests__/useTrack.test.tsx b/src/__tests__/useTrack.test.tsx index 11ecce4..4447189 100644 --- a/src/__tests__/useTrack.test.tsx +++ b/src/__tests__/useTrack.test.tsx @@ -10,7 +10,7 @@ import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ -import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { useTrack } from '../useTrack'; @@ -21,19 +21,19 @@ describe('useTrack', () => { const value = 10; const properties = { prop1: 'prop1' }; - test('returns the track method bound to the client at Split context updated by SplitFactory.', () => { + test('returns the track method bound to the client at Split context updated by SplitFactoryProvider.', () => { const outerFactory = SplitSdk(sdkBrowser); let boundTrack; let trackResult; render( - + {React.createElement(() => { boundTrack = useTrack(); trackResult = boundTrack(tt, eventType, value, properties); return null; })} - , + , ); const track = outerFactory.client().track as jest.Mock; expect(track).toBeCalledWith(tt, eventType, value, properties); @@ -46,7 +46,7 @@ describe('useTrack', () => { let trackResult; render( - + {React.createElement(() => { boundTrack = useTrack(); @@ -54,7 +54,7 @@ describe('useTrack', () => { return null; })} - + ); const track = outerFactory.client('user2').track as jest.Mock; expect(track).toBeCalledWith(tt, eventType, value, properties); @@ -67,13 +67,13 @@ describe('useTrack', () => { let trackResult; render( - + {React.createElement(() => { boundTrack = useTrack('user2', tt); trackResult = boundTrack(eventType, value, properties); return null; })} - , + , ); const track = outerFactory.client('user2', tt).track as jest.Mock; expect(track).toBeCalledWith(eventType, value, properties); diff --git a/src/__tests__/withSplitFactory.test.tsx b/src/__tests__/withSplitFactory.test.tsx index 32813b0..6df93f6 100644 --- a/src/__tests__/withSplitFactory.test.tsx +++ b/src/__tests__/withSplitFactory.test.tsx @@ -15,7 +15,7 @@ jest.mock('../SplitFactory'); import { ISplitFactoryChildProps } from '../types'; import { withSplitFactory } from '../withSplitFactory'; -describe('SplitFactory', () => { +describe('withSplitFactory', () => { test('passes no-ready props to the child if initialized with a no ready factory (e.g., using config object).', () => { const Component = withSplitFactory(sdkBrowser)( diff --git a/src/constants.ts b/src/constants.ts index 7d81db7..c29d802 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,14 +14,11 @@ export const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig = { }; // 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 SplitFactoryProvider. Config prop will be ignored.'; +// @TODO remove with SplitFactory component in next major version. SplitFactoryProvider can accept no props and eventually only an initialState 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 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 properties were provided. flagSets will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 83d4b26..5fc7cac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,12 +45,17 @@ export interface ISplitStatus { export interface ISplitContextValues extends ISplitStatus { /** - * Split factory instance + * Split factory instance. + * + * NOTE: This property is not recommended for direct use, as better alternatives are available. */ factory: SplitIO.IBrowserSDK | null; /** - * Split client instance + * Split client instance. + * + * NOTE: This property is not recommended for direct use, as better alternatives are available. + * * @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; @@ -94,14 +99,14 @@ export interface IUpdateProps { } /** - * SplitFactory Child Props interface. These are the props that the child component receives from the 'SplitFactory' component. + * SplitFactoryProvider Child Props interface. These are the props that the child component receives from the 'SplitFactoryProvider' component. */ -// @TODO remove next type (breaking-change) +// @TODO rename/remove next type (breaking-change) // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ISplitFactoryChildProps extends ISplitContextValues { } /** - * SplitFactory Props interface. These are the props accepted by SplitFactory component, + * SplitFactoryProvider Props interface. These are the props accepted by SplitFactoryProvider component, * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { @@ -114,6 +119,8 @@ export interface ISplitFactoryProps extends IUpdateProps { /** * Split factory instance to use instead of creating a new one with the config object. + * + * If both `factory` and `config` are provided, the `config` option is ignored. */ factory?: SplitIO.IBrowserSDK; @@ -123,7 +130,7 @@ export interface ISplitFactoryProps extends IUpdateProps { attributes?: SplitIO.Attributes; /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. + * Children of the SplitFactoryProvider component. It can be a functional component (child as a function) or a React element. */ children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode; } @@ -165,7 +172,7 @@ export interface ISplitClientChildProps extends ISplitContextValues { } 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 of the SplitClient component. It can be a functional component (child as a function) or a React element. */ children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } diff --git a/src/useClient.ts b/src/useClient.ts index 535eb98..7428b60 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -3,9 +3,9 @@ import { useSplitClient } from './useSplitClient'; /** * '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. + * SplitFactoryProvider and SplitClient components in the hierarchy of components. * - * @returns 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 SplitFactoryProvider or factory is not ready. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} * diff --git a/src/useManager.ts b/src/useManager.ts index 9ba101b..094ec99 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -3,9 +3,9 @@ import { useSplitManager } from './useSplitManager'; /** * '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. + * the SplitFactoryProvider component. * - * @returns 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 SplitFactoryProvider or factory is not ready. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 3ff8562..5428aa7 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -12,7 +12,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. + * It uses the 'useContext' hook to access the context, which is updated by SplitFactoryProvider and SplitClient components in the hierarchy of components. * * @returns A Split Context object * diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index 1ba6a23..13aa9b1 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -4,9 +4,9 @@ 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. + * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactoryProvider component. * - * @returns 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 SplitFactoryProvider or factory is not ready. * * @example * ```js @@ -16,7 +16,7 @@ import { ISplitContextValues } from './types'; * @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. + // Update options are not supported, because updates can be controlled at the SplitFactoryProvider component. const context = React.useContext(SplitContext); return { ...context,