diff --git a/package-lock.json b/package-lock.json index 6e79190..99d919c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.10.2", + "version": "1.10.3-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.10.2", + "version": "1.10.3-rc.3", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.24.1", + "@splitsoftware/splitio": "10.25.1-rc.0", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -1547,17 +1547,17 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.24.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.1.tgz", - "integrity": "sha512-WzVZrP2IAqzNBywNXgmLxiS60qumkcnu6u1lUPlNgdVek82TzWeqyqW+htKmDMJ/ifsJPWrgT1VLMZJvOnBsVA==", + "version": "10.25.1-rc.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.25.1-rc.0.tgz", + "integrity": "sha512-lSfNKoloima/kjtn1W/fJFj9l0WuwE+iSOKgRn13AgWdSE7vdr71qJ0ZLuwmk0HLjv52OnAgSGi0DbS+0qM6Ow==", "dependencies": { - "@splitsoftware/splitio-commons": "1.12.1", + "@splitsoftware/splitio-commons": "1.13.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "unfetch": "^4.2.0" }, "engines": { @@ -1569,9 +1569,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.12.1.tgz", - "integrity": "sha512-EkCcqlYvVafazs9c5i+pmhf6rIyj3A70dqQ4U3BKE646t7tf6mxGzqZz1sAl540xNyYI7CA/iIqisEWvDtJc0A==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.13.1.tgz", + "integrity": "sha512-xGu94sLx+tJb6PeM26vH8/LEElsaVbh2BjoLvL5twR4gKsVezie5ZtHhejWT1+iCVCtJuhjZxKwOm4HGYoVIHQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -8353,9 +8353,9 @@ "dev": true }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -12089,25 +12089,25 @@ } }, "@splitsoftware/splitio": { - "version": "10.24.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.1.tgz", - "integrity": "sha512-WzVZrP2IAqzNBywNXgmLxiS60qumkcnu6u1lUPlNgdVek82TzWeqyqW+htKmDMJ/ifsJPWrgT1VLMZJvOnBsVA==", + "version": "10.25.1-rc.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.25.1-rc.0.tgz", + "integrity": "sha512-lSfNKoloima/kjtn1W/fJFj9l0WuwE+iSOKgRn13AgWdSE7vdr71qJ0ZLuwmk0HLjv52OnAgSGi0DbS+0qM6Ow==", "requires": { - "@splitsoftware/splitio-commons": "1.12.1", + "@splitsoftware/splitio-commons": "1.13.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", "eventsource": "^1.1.2", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "unfetch": "^4.2.0" } }, "@splitsoftware/splitio-commons": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.12.1.tgz", - "integrity": "sha512-EkCcqlYvVafazs9c5i+pmhf6rIyj3A70dqQ4U3BKE646t7tf6mxGzqZz1sAl540xNyYI7CA/iIqisEWvDtJc0A==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.13.1.tgz", + "integrity": "sha512-xGu94sLx+tJb6PeM26vH8/LEElsaVbh2BjoLvL5twR4gKsVezie5ZtHhejWT1+iCVCtJuhjZxKwOm4HGYoVIHQ==", "requires": { "tslib": "^2.3.1" } @@ -17232,9 +17232,9 @@ "dev": true }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" }, diff --git a/package.json b/package.json index 41ed506..f1608a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.10.2", + "version": "1.10.3-rc.3", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.24.1", + "@splitsoftware/splitio": "10.25.1-rc.0", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx index ba2c12d..f9e5e03 100644 --- a/src/SplitFactory.tsx +++ b/src/SplitFactory.tsx @@ -15,6 +15,8 @@ import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; * 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} + * + * @deprecated Replace with the new `SplitFactoryProvider` component. */ export class SplitFactory extends React.Component { diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx new file mode 100644 index 0000000..d3ac78b --- /dev/null +++ b/src/SplitFactoryProvider.tsx @@ -0,0 +1,74 @@ +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 { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; + +/** + * SplitFactoryProvider will initialize the Split SDK and its main client when it is mounted, listen for its events in order to update the Split Context, + * and automatically shutdown and release resources when it is unmounted. SplitFactoryProvider must wrap other library components and functions + * since they access the Split Context and its elements (factory, clients, etc). + * + * NOTE: Either pass a factory instance or a config object. If both are passed, the config object will be ignored. + * Pass the same reference to the config or factory object rather than a new instance on each render, to avoid unnecessary props changes and SDK reinitializations. + * + * @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} + */ +export function SplitFactoryProvider(props: ISplitFactoryProps) { + let { + config, factory: propFactory, + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate + } = { ...DEFAULT_UPDATE_OPTIONS, ...props }; + + if (config && propFactory) { + console.log(WARN_SF_CONFIG_AND_FACTORY); + config = undefined; + } + + const [stateFactory, setStateFactory] = React.useState(propFactory || null); + const factory = propFactory || stateFactory; + const client = factory ? getSplitClient(factory) : null; + + React.useEffect(() => { + if (config) { + const factory = getSplitFactory(config); + const client = getSplitClient(factory); + const status = getStatus(client); + + // Update state and unsubscribe from events when first event is emitted + const update = () => { + 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) { + if (status.isReady) update(); + else client.once(client.Event.SDK_READY, update); + } + if (updateOnSdkReadyFromCache) { + if (status.isReadyFromCache) update(); + else client.once(client.Event.SDK_READY_FROM_CACHE, update); + } + if (updateOnSdkTimedout) { + if (status.hasTimedout) update(); + else client.once(client.Event.SDK_READY_TIMED_OUT, update); + } + if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); + + return () => { + // Factory destroy unsubscribes from events + destroySplitFactory(factory as IFactoryWithClients); + } + } + }, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); + + return ( + + ); +} diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx new file mode 100644 index 0000000..2964a60 --- /dev/null +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -0,0 +1,401 @@ +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'; +const logSpy = jest.spyOn(console, 'log'); + +/** Test target */ +import { ISplitFactoryChildProps } from '../types'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; +import { SplitClient } from '../SplitClient'; +import { SplitContext } from '../SplitContext'; +import { __factories } from '../utils'; +import { WARN_SF_CONFIG_AND_FACTORY } from '../constants'; + +describe('SplitFactoryProvider', () => { + + test('passes no-ready props to the child if initialized with a config.', () => { + render( + + {({ factory, client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { + expect(factory).toBe(null); + expect(client).toBe(null); + expect(isReady).toBe(false); + expect(isReadyFromCache).toBe(false); + expect(hasTimedout).toBe(false); + expect(isTimedout).toBe(false); + expect(isDestroyed).toBe(false); + expect(lastUpdate).toBe(0); + return null; + }} + + ); + }); + + test('passes ready props to the child if initialized with a ready factory.', async () => { + const outerFactory = SplitSdk(sdkBrowser); + (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); + (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); + (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); + await outerFactory.client().ready(); + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { + expect(factory).toBe(outerFactory); + expect(isReady).toBe(true); + expect(isReadyFromCache).toBe(true); + expect(hasTimedout).toBe(false); + expect(isTimedout).toBe(false); + expect(isDestroyed).toBe(false); + expect(lastUpdate).toBe(0); + expect((factory as SplitIO.ISDK).settings.version).toBe(outerFactory.settings.version); + return null; + }} + + ); + }); + + test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Ready from cache + expect(statusProps).toStrictEqual([false, true, true, true]); + break; + case 3: // Ready + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + case 4: // Updated + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + if (factory) expect(factory).toBe(innerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(5); + }); + + test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events (factory prop)', async () => { + const outerFactory = SplitSdk(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Ready from cache + expect(statusProps).toStrictEqual([false, true, true, true]); + break; + case 3: // Ready + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + case 4: // Updated + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + 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); + }); + + test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. + expect(statusProps).toStrictEqual([true, false, true, false]); + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + if (factory) expect(factory).toBe(innerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(3); + }); + + test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (factory prop)', async () => { + const outerFactory = SplitSdk(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. + expect(statusProps).toStrictEqual([true, false, true, false]); + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + 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); + }); + + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Ready + expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + if (factory) expect(factory).toBe(innerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + expect(renderTimes).toBe(2); + }); + + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (factory prop)', async () => { + const outerFactory = SplitSdk(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Ready + expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + ); + + 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(2); + }); + + test('renders a passed JSX.Element with a new SplitContext value.', (done) => { + const Component = () => { + return ( + + {(value) => { + expect(value.factory).toBe(null); + expect(value.client).toBe(null); + expect(value.isReady).toBe(false); + expect(value.isTimedout).toBe(false); + expect(value.lastUpdate).toBe(0); + done(); + return null; + }} + + ); + }; + + render( + + + + ); + }); + + test('logs warning if both a config and factory are passed as props.', () => { + const outerFactory = SplitSdk(sdkBrowser); + + render( + + {({ factory }) => { + expect(factory).toBe(outerFactory); + return null; + }} + + ); + + expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY); + logSpy.mockRestore(); + }); + + test('cleans up on unmount.', () => { + let destroyMainClientSpy; + let destroySharedClientSpy; + const wrapper = render( + + {({ factory }) => { + if (!factory) return null; // 1st render + + // 2nd render (SDK ready) + expect(__factories.size).toBe(1); + destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); + return ( + + {({ client }) => { + destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); + return null; + }} + + ); + }} + + ); + + // SDK ready to re-render + act(() => { + const factory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + factory.client().__emitter__.emit(Event.SDK_READY) + }); + + wrapper.unmount(); + // the factory created by the component is removed from `factories` cache and its clients are destroyed + expect(__factories.size).toBe(0); + expect(destroyMainClientSpy).toBeCalledTimes(1); + expect(destroySharedClientSpy).toBeCalledTimes(1); + }); + + test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => { + let destroyMainClientSpy; + let destroySharedClientSpy; + const outerFactory = SplitSdk(sdkBrowser); + const wrapper = render( + + {({ 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'); + return ( + + {({ client }) => { + destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); + return null; + }} + + ); + }} + + ); + wrapper.unmount(); + expect(destroyMainClientSpy).not.toBeCalled(); + expect(destroySharedClientSpy).not.toBeCalled(); + }); + +}); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index c8bd2f3..b0b3538 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -3,6 +3,7 @@ import { SplitContext as ExportedSplitContext, SplitSdk as ExportedSplitSdk, SplitFactory as ExportedSplitFactory, + SplitFactoryProvider as ExportedSplitFactoryProvider, SplitClient as ExportedSplitClient, SplitTreatments as ExportedSplitTreatments, withSplitFactory as exportedWithSplitFactory, @@ -32,6 +33,7 @@ import { import { SplitContext } from '../SplitContext'; import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client'; import { SplitFactory } from '../SplitFactory'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitTreatments } from '../SplitTreatments'; import { withSplitFactory } from '../withSplitFactory'; @@ -49,6 +51,7 @@ describe('index', () => { it('should export components', () => { expect(ExportedSplitFactory).toBe(SplitFactory); + expect(ExportedSplitFactoryProvider).toBe(SplitFactoryProvider); expect(ExportedSplitClient).toBe(SplitClient); expect(ExportedSplitTreatments).toBe(SplitTreatments); }); diff --git a/src/index.ts b/src/index.ts index 1e2dddd..10a5161 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,11 @@ export { withSplitFactory } from './withSplitFactory'; export { withSplitClient } from './withSplitClient'; export { withSplitTreatments } from './withSplitTreatments'; -// Render props components +// Components export { SplitTreatments } from './SplitTreatments'; export { SplitClient } from './SplitClient'; export { SplitFactory } from './SplitFactory'; +export { SplitFactoryProvider } from './SplitFactoryProvider'; // Hooks export { useClient } from './useClient'; diff --git a/src/withSplitFactory.tsx b/src/withSplitFactory.tsx index bd939e2..076ecea 100644 --- a/src/withSplitFactory.tsx +++ b/src/withSplitFactory.tsx @@ -9,6 +9,8 @@ import { SplitFactory } from './SplitFactory'; * * @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. + * + * @deprecated Use `SplitFactoryProvider` instead. */ export function withSplitFactory(config?: SplitIO.IBrowserSettings, factory?: SplitIO.IBrowserSDK, attributes?: SplitIO.Attributes) { diff --git a/umd.ts b/umd.ts index 1d67908..6beea58 100644 --- a/umd.ts +++ b/umd.ts @@ -1,15 +1,15 @@ import { SplitSdk, withSplitFactory, withSplitClient, withSplitTreatments, - SplitFactory, SplitClient, SplitTreatments, - useClient, useTreatments, useTrack, useManager, + SplitFactory, SplitFactoryProvider, SplitClient, SplitTreatments, + useClient, useSplitClient, useTreatments, useSplitTreatments, useTrack, useManager, useSplitManager, SplitContext, } from './src/index'; export default { SplitSdk, withSplitFactory, withSplitClient, withSplitTreatments, - SplitFactory, SplitClient, SplitTreatments, - useClient, useTreatments, useTrack, useManager, + SplitFactory, SplitFactoryProvider, SplitClient, SplitTreatments, + useClient, useSplitClient, useTreatments, useSplitTreatments, useTrack, useManager, useSplitManager, SplitContext, };