Skip to content

Commit 045c835

Browse files
Merge pull request #181 from splitio/add_SplitFactoryProvider
Polishing: replace `SplitFactory` for `SplitFactoryProvider` in comments and tests
2 parents 068f2a2 + 1504d59 commit 045c835

14 files changed

+168
-88
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
1.11.0 (January 15, 2023)
2+
- Added new `SplitFactoryProvider` component as replacement for the now deprecated `SplitFactory` component.
3+
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).
4+
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).
5+
- Updated internal code to remove a circular dependency and avoid warning messages with tools like PNPM (Related to issue #176).
6+
17
1.10.2 (December 12, 2023)
28
- 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.
39

src/SplitContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export const INITIAL_CONTEXT: ISplitContextValues = {
1818
/**
1919
* Split Context is the React Context instance that represents our SplitIO global state.
2020
* It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate)
21-
* The context is created with default empty values, that eventually SplitFactory and SplitClient access and update.
21+
* The context is created with default empty values, that SplitFactoryProvider and SplitClient access and update.
2222
*/
2323
export const SplitContext = React.createContext<ISplitContextValues>(INITIAL_CONTEXT);

src/SplitFactoryProvider.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { SplitComponent } from './SplitClient';
44
import { ISplitFactoryProps } from './types';
55
import { WARN_SF_CONFIG_AND_FACTORY } from './constants';
6-
import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus } from './utils';
6+
import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus, __factories } from './utils';
77
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
88

99
/**
@@ -27,24 +27,39 @@ export function SplitFactoryProvider(props: ISplitFactoryProps) {
2727
config = undefined;
2828
}
2929

30-
const [stateFactory, setStateFactory] = React.useState(propFactory || null);
31-
const factory = propFactory || stateFactory;
30+
const [configFactory, setConfigFactory] = React.useState<IFactoryWithClients | null>(null);
31+
const factory = propFactory || (configFactory && config === configFactory.config ? configFactory : null);
3232
const client = factory ? getSplitClient(factory) : null;
3333

34+
// Effect to initialize and destroy the factory
3435
React.useEffect(() => {
3536
if (config) {
3637
const factory = getSplitFactory(config);
38+
39+
return () => {
40+
destroySplitFactory(factory);
41+
}
42+
}
43+
}, [config]);
44+
45+
// Effect to subscribe/unsubscribe to events
46+
React.useEffect(() => {
47+
const factory = config && __factories.get(config);
48+
if (factory) {
3749
const client = getSplitClient(factory);
3850
const status = getStatus(client);
3951

40-
// Update state and unsubscribe from events when first event is emitted
41-
const update = () => {
52+
// Unsubscribe from events and update state when first event is emitted
53+
const update = () => { // eslint-disable-next-line no-use-before-define
54+
unsubscribe();
55+
setConfigFactory(factory);
56+
}
57+
58+
const unsubscribe = () => {
4259
client.off(client.Event.SDK_READY, update);
4360
client.off(client.Event.SDK_READY_FROM_CACHE, update);
4461
client.off(client.Event.SDK_READY_TIMED_OUT, update);
4562
client.off(client.Event.SDK_UPDATE, update);
46-
47-
setStateFactory(factory);
4863
}
4964

5065
if (updateOnSdkReady) {
@@ -61,10 +76,7 @@ export function SplitFactoryProvider(props: ISplitFactoryProps) {
6176
}
6277
if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update);
6378

64-
return () => {
65-
// Factory destroy unsubscribes from events
66-
destroySplitFactory(factory as IFactoryWithClients);
67-
}
79+
return unsubscribe;
6880
}
6981
}, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]);
7082

src/__tests__/SplitFactoryProvider.test.tsx

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -336,40 +336,95 @@ describe('SplitFactoryProvider', () => {
336336
logSpy.mockRestore();
337337
});
338338

339-
test('cleans up on unmount.', () => {
340-
let destroyMainClientSpy;
341-
let destroySharedClientSpy;
342-
const wrapper = render(
343-
<SplitFactoryProvider config={sdkBrowser} >
344-
{({ factory }) => {
345-
if (!factory) return null; // 1st render
339+
test('cleans up on update and unmount if config prop is provided.', () => {
340+
let renderTimes = 0;
341+
const createdFactories = new Set<SplitIO.ISDK>();
342+
const clientDestroySpies: jest.SpyInstance[] = [];
343+
const outerFactory = SplitSdk(sdkBrowser);
344+
345+
const Component = ({ factory, isReady, hasTimedout }: ISplitFactoryChildProps) => {
346+
renderTimes++;
346347

347-
// 2nd render (SDK ready)
348-
expect(__factories.size).toBe(1);
349-
destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy');
348+
switch (renderTimes) {
349+
case 1:
350+
expect(factory).toBe(outerFactory);
351+
return null;
352+
case 2:
353+
case 5:
354+
expect(isReady).toBe(false);
355+
expect(hasTimedout).toBe(false);
356+
expect(factory).toBe(null);
357+
return null;
358+
case 3:
359+
case 4:
360+
case 6:
361+
expect(isReady).toBe(true);
362+
expect(hasTimedout).toBe(true);
363+
expect(factory).not.toBe(null);
364+
createdFactories.add(factory!);
365+
clientDestroySpies.push(jest.spyOn(factory!.client(), 'destroy'));
350366
return (
351367
<SplitClient splitKey='other_key' >
352368
{({ client }) => {
353-
destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy');
369+
clientDestroySpies.push(jest.spyOn(client!, 'destroy'));
354370
return null;
355371
}}
356372
</SplitClient>
357373
);
358-
}}
359-
</SplitFactoryProvider>
360-
);
374+
case 7:
375+
throw new Error('Must not rerender');
376+
}
377+
};
361378

362-
// SDK ready to re-render
363-
act(() => {
379+
const emitSdkEvents = () => {
364380
const factory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value;
381+
factory.client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)
365382
factory.client().__emitter__.emit(Event.SDK_READY)
366-
});
383+
};
384+
385+
// 1st render: factory provided
386+
const wrapper = render(
387+
<SplitFactoryProvider factory={outerFactory} >
388+
{Component}
389+
</SplitFactoryProvider>
390+
);
391+
392+
// 2nd render: factory created, not ready (null)
393+
wrapper.rerender(
394+
<SplitFactoryProvider config={sdkBrowser} >
395+
{Component}
396+
</SplitFactoryProvider>
397+
);
398+
399+
// 3rd render: SDK ready (timeout is ignored due to updateOnSdkTimedout=false)
400+
act(emitSdkEvents);
401+
402+
// 4th render: same config prop -> factory is not recreated
403+
wrapper.rerender(
404+
<SplitFactoryProvider config={sdkBrowser} updateOnSdkReady={false} updateOnSdkTimedout={true} >
405+
{Component}
406+
</SplitFactoryProvider>
407+
);
408+
409+
act(emitSdkEvents); // Emitting events again has no effect
410+
expect(createdFactories.size).toBe(1);
411+
412+
// 5th render: Update config prop -> factory is recreated, not ready yet (null)
413+
wrapper.rerender(
414+
<SplitFactoryProvider config={{ ...sdkBrowser }} updateOnSdkReady={false} updateOnSdkTimedout={true} >
415+
{Component}
416+
</SplitFactoryProvider>
417+
);
418+
419+
// 6th render: SDK timeout (ready is ignored due to updateOnSdkReady=false)
420+
act(emitSdkEvents);
367421

368422
wrapper.unmount();
369-
// the factory created by the component is removed from `factories` cache and its clients are destroyed
423+
424+
// Created factories are removed from `factories` cache and their clients are destroyed
425+
expect(createdFactories.size).toBe(2);
370426
expect(__factories.size).toBe(0);
371-
expect(destroyMainClientSpy).toBeCalledTimes(1);
372-
expect(destroySharedClientSpy).toBeCalledTimes(1);
427+
clientDestroySpies.forEach(spy => expect(spy).toBeCalledTimes(1));
373428
});
374429

375430
test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => {
@@ -381,11 +436,11 @@ describe('SplitFactoryProvider', () => {
381436
{({ factory }) => {
382437
// if factory is provided as a prop, `factories` cache is not modified
383438
expect(__factories.size).toBe(0);
384-
destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy');
439+
destroyMainClientSpy = jest.spyOn(factory!.client(), 'destroy');
385440
return (
386441
<SplitClient splitKey='other_key' >
387442
{({ client }) => {
388-
destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy');
443+
destroySharedClientSpy = jest.spyOn(client!, 'destroy');
389444
return null;
390445
}}
391446
</SplitClient>

src/__tests__/useSplitClient.test.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@ import { sdkBrowser } from './testUtils/sdkConfigs';
1111

1212
/** Test target */
1313
import { useSplitClient } from '../useSplitClient';
14-
import { SplitFactory } from '../SplitFactory';
14+
import { SplitFactoryProvider } from '../SplitFactoryProvider';
1515
import { SplitClient } from '../SplitClient';
1616
import { SplitContext } from '../SplitContext';
1717
import { testAttributesBinding, TestComponentProps } from './testUtils/utils';
1818

1919
describe('useSplitClient', () => {
2020

21-
test('returns the main client from the context updated by SplitFactory.', () => {
21+
test('returns the main client from the context updated by SplitFactoryProvider.', () => {
2222
const outerFactory = SplitSdk(sdkBrowser);
2323
let client;
2424
render(
25-
<SplitFactory factory={outerFactory} >
25+
<SplitFactoryProvider factory={outerFactory} >
2626
{React.createElement(() => {
2727
client = useSplitClient().client;
2828
return null;
2929
})}
30-
</SplitFactory>
30+
</SplitFactoryProvider>
3131
);
3232
expect(client).toBe(outerFactory.client());
3333
});
@@ -36,14 +36,14 @@ describe('useSplitClient', () => {
3636
const outerFactory = SplitSdk(sdkBrowser);
3737
let client;
3838
render(
39-
<SplitFactory factory={outerFactory} >
39+
<SplitFactoryProvider factory={outerFactory} >
4040
<SplitClient splitKey='user2' >
4141
{React.createElement(() => {
4242
client = useSplitClient().client;
4343
return null;
4444
})}
4545
</SplitClient>
46-
</SplitFactory>
46+
</SplitFactoryProvider>
4747
);
4848
expect(client).toBe(outerFactory.client('user2'));
4949
});
@@ -52,13 +52,13 @@ describe('useSplitClient', () => {
5252
const outerFactory = SplitSdk(sdkBrowser);
5353
let client;
5454
render(
55-
<SplitFactory factory={outerFactory} >
55+
<SplitFactoryProvider factory={outerFactory} >
5656
{React.createElement(() => {
5757
(outerFactory.client as jest.Mock).mockClear();
5858
client = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client;
5959
return null;
6060
})}
61-
</SplitFactory>
61+
</SplitFactoryProvider>
6262
);
6363
expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user');
6464
expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client);
@@ -89,9 +89,9 @@ describe('useSplitClient', () => {
8989

9090
function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) {
9191
return (
92-
<SplitFactory factory={factory} attributes={attributesFactory} >
92+
<SplitFactoryProvider factory={factory} attributes={attributesFactory} >
9393
<InnerComponent splitKey={splitKey} attributesClient={attributesClient} testSwitch={testSwitch} />
94-
</SplitFactory>
94+
</SplitFactoryProvider>
9595
);
9696
}
9797

@@ -108,21 +108,21 @@ describe('useSplitClient', () => {
108108
let countNestedComponent = 0;
109109

110110
render(
111-
<SplitFactory factory={outerFactory} >
111+
<SplitFactoryProvider factory={outerFactory} >
112112
<>
113113
<SplitContext.Consumer>
114114
{() => countSplitContext++}
115115
</SplitContext.Consumer>
116116
<SplitClient splitKey={sdkBrowser.core.key} trafficType={sdkBrowser.core.trafficType}
117-
/* Disabling update props is ineffective because the wrapping SplitFactory has them enabled: */
117+
/* Disabling update props is ineffective because the wrapping SplitFactoryProvider has them enabled: */
118118
updateOnSdkReady={false} updateOnSdkReadyFromCache={false}
119119
>
120120
{() => { countSplitClient++; return null }}
121121
</SplitClient>
122122
{React.createElement(() => {
123123
// Equivalent to
124124
// - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });`
125-
// - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });`
125+
// - Disabling update props, since the wrapping SplitFactoryProvider has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });`
126126
const { client } = useSplitClient({ attributes: { att1: 'att1' } });
127127
expect(client).toBe(mainClient); // Assert that the main client was retrieved.
128128
expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes.
@@ -183,7 +183,7 @@ describe('useSplitClient', () => {
183183
})}
184184
</SplitClient>
185185
</>
186-
</SplitFactory>
186+
</SplitFactoryProvider>
187187
);
188188

189189
act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
@@ -226,7 +226,7 @@ describe('useSplitClient', () => {
226226
let count = 0;
227227

228228
render(
229-
<SplitFactory factory={outerFactory} >
229+
<SplitFactoryProvider factory={outerFactory} >
230230
{React.createElement(() => {
231231
useSplitClient({ splitKey: 'some_user' });
232232
count++;
@@ -237,7 +237,7 @@ describe('useSplitClient', () => {
237237

238238
return null;
239239
})}
240-
</SplitFactory>
240+
</SplitFactoryProvider>
241241
)
242242

243243
expect(count).toEqual(2);
@@ -257,9 +257,9 @@ describe('useSplitClient', () => {
257257

258258
function Component(updateOptions) {
259259
return (
260-
<SplitFactory factory={outerFactory} >
260+
<SplitFactoryProvider factory={outerFactory} >
261261
<InnerComponent {...updateOptions} />
262-
</SplitFactory>
262+
</SplitFactoryProvider>
263263
)
264264
}
265265

src/__tests__/useSplitManager.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { sdkBrowser } from './testUtils/sdkConfigs';
1111
import { getStatus } from '../utils';
1212

1313
/** Test target */
14-
import { SplitFactory } from '../SplitFactory';
14+
import { SplitFactoryProvider } from '../SplitFactoryProvider';
1515
import { useSplitManager } from '../useSplitManager';
1616

1717
describe('useSplitManager', () => {
@@ -20,12 +20,12 @@ describe('useSplitManager', () => {
2020
const outerFactory = SplitSdk(sdkBrowser);
2121
let hookResult;
2222
render(
23-
<SplitFactory factory={outerFactory} >
23+
<SplitFactoryProvider factory={outerFactory} >
2424
{React.createElement(() => {
2525
hookResult = useSplitManager();
2626
return null;
2727
})}
28-
</SplitFactory>
28+
</SplitFactoryProvider>
2929
);
3030

3131
expect(hookResult).toStrictEqual({

0 commit comments

Comments
 (0)