Skip to content

Commit d50d900

Browse files
Merge pull request #183 from splitio/fix_logs
Update logs for `SplitFactoryProvider` component
2 parents 0b869f9 + 045c835 commit d50d900

22 files changed

+243
-182
lines changed

.github/workflows/ci.yml renamed to .github/workflows/ci-cd.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ jobs:
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: Checkout code
26-
uses: actions/checkout@v3
26+
uses: actions/checkout@v4
2727
with:
2828
fetch-depth: 0
2929

3030
- name: Set up nodejs
3131
uses: actions/setup-node@v3
3232
with:
33-
node-version: '16.16.0'
33+
node-version: 'lts/*'
3434
cache: 'npm'
3535

3636
- name: npm ci

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v16.16.0
1+
lts/*

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

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Below is a simple example that describes the instantiation and most basic usage
2020
import React from 'react';
2121

2222
// Import SDK functions
23-
import { SplitFactory, useSplitTreatments } from '@splitsoftware/splitio-react';
23+
import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react';
2424

2525
// Define your config object
2626
const CONFIG = {
@@ -48,10 +48,10 @@ function MyComponent() {
4848

4949
function MyApp() {
5050
return (
51-
// Use SplitFactory to instantiate the SDK and makes it available to nested components
52-
<SplitFactory config={CONFIG} >
51+
// Use SplitFactoryProvider to instantiate the SDK and makes it available to nested components
52+
<SplitFactoryProvider config={CONFIG} >
5353
<MyComponent />
54-
</SplitFactory>
54+
</SplitFactoryProvider>
5555
);
5656
}
5757
```

src/SplitClient.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React from 'react';
22
import { SplitContext } from './SplitContext';
33
import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types';
4-
import { ERROR_SC_NO_FACTORY } from './constants';
54
import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils';
65
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
76

87
/**
98
* Common component used to handle the status and events of a Split client passed as prop.
10-
* Reused by both SplitFactory (main client) and SplitClient (shared client) components.
9+
* Reused by both SplitFactoryProvider (main client) and SplitClient (any client) components.
1110
*/
1211
export class SplitComponent extends React.Component<IUpdateProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes, children: any }, ISplitContextValues> {
1312

@@ -47,11 +46,6 @@ export class SplitComponent extends React.Component<IUpdateProps & { factory: Sp
4746
super(props);
4847
const { factory, client } = props;
4948

50-
// Log error if factory is not available
51-
if (!factory) {
52-
console.error(ERROR_SC_NO_FACTORY);
53-
}
54-
5549
this.state = {
5650
factory,
5751
client,
@@ -129,9 +123,8 @@ export class SplitComponent extends React.Component<IUpdateProps & { factory: Sp
129123
* SplitClient will initialize a new SDK client and listen for its events in order to update the Split Context.
130124
* Children components will have access to the new client when accessing Split Context.
131125
*
132-
* Unlike SplitFactory, the underlying SDK client can be changed during the component lifecycle
133-
* if the component is updated with a different splitKey or trafficType prop. Since the client can change,
134-
* its release is not handled by SplitClient but by its container SplitFactory component.
126+
* The underlying SDK client can be changed during the component lifecycle
127+
* if the component is updated with a different splitKey or trafficType prop.
135128
*
136129
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
137130
*/

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/SplitTreatments.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import { SplitContext } from './SplitContext';
33
import { ISplitTreatmentsProps, ISplitContextValues } from './types';
4-
import { WARN_ST_NO_CLIENT } from './constants';
54
import { memoizeGetTreatmentsWithConfig } from './utils';
65

76
/**
@@ -26,7 +25,7 @@ export class SplitTreatments extends React.Component<ISplitTreatmentsProps> {
2625
{(splitContext: ISplitContextValues) => {
2726
const { client, lastUpdate } = splitContext;
2827
const treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets);
29-
if (!client) { this.logWarning = true; }
28+
3029
// SplitTreatments only accepts a function as a child, not a React Element (JSX)
3130
return children({
3231
...splitContext, treatments,
@@ -35,9 +34,4 @@ export class SplitTreatments extends React.Component<ISplitTreatmentsProps> {
3534
</SplitContext.Consumer>
3635
);
3736
}
38-
39-
componentDidMount() {
40-
if (this.logWarning) { console.log(WARN_ST_NO_CLIENT); }
41-
}
42-
4337
}

src/__tests__/SplitClient.test.tsx

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

1212
/** Test target */
1313
import { ISplitClientChildProps } from '../types';
14-
import { SplitFactory } from '../SplitFactory';
14+
import { SplitFactoryProvider } from '../SplitFactoryProvider';
1515
import { SplitClient } from '../SplitClient';
1616
import { SplitContext } from '../SplitContext';
17-
import { ERROR_SC_NO_FACTORY } from '../constants';
1817
import { testAttributesBinding, TestComponentProps } from './testUtils/utils';
1918

2019
describe('SplitClient', () => {
2120

2221
test('passes no-ready props to the child if client is not ready.', () => {
2322
render(
24-
<SplitFactory config={sdkBrowser} >
23+
<SplitFactoryProvider config={sdkBrowser} >
2524
<SplitClient splitKey='user1' >
2625
{({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => {
2726
expect(isReady).toBe(false);
@@ -34,7 +33,7 @@ describe('SplitClient', () => {
3433
return null;
3534
}}
3635
</SplitClient>
37-
</SplitFactory>
36+
</SplitFactoryProvider>
3837
);
3938
});
4039

@@ -46,7 +45,7 @@ describe('SplitClient', () => {
4645
await outerFactory.client().ready();
4746

4847
render(
49-
<SplitFactory factory={outerFactory} >
48+
<SplitFactoryProvider factory={outerFactory} >
5049
{/* Equivalent to <SplitClient splitKey={undefined} > */}
5150
<SplitClient splitKey={sdkBrowser.core.key} >
5251
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => {
@@ -61,7 +60,7 @@ describe('SplitClient', () => {
6160
return null;
6261
}}
6362
</SplitClient>
64-
</SplitFactory>
63+
</SplitFactoryProvider>
6564
);
6665
});
6766

@@ -76,7 +75,7 @@ describe('SplitClient', () => {
7675
let previousLastUpdate = -1;
7776

7877
render(
79-
<SplitFactory factory={outerFactory} >
78+
<SplitFactoryProvider factory={outerFactory} >
8079
<SplitClient splitKey='user2' updateOnSdkTimedout={true} updateOnSdkUpdate={true} >
8180
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
8281
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
@@ -106,7 +105,7 @@ describe('SplitClient', () => {
106105
return null;
107106
}}
108107
</SplitClient>
109-
</SplitFactory>
108+
</SplitFactoryProvider>
110109
);
111110

112111
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
@@ -128,7 +127,7 @@ describe('SplitClient', () => {
128127
let previousLastUpdate = -1;
129128

130129
render(
131-
<SplitFactory factory={outerFactory} >
130+
<SplitFactoryProvider factory={outerFactory} >
132131
<SplitClient splitKey='user2' updateOnSdkReady={false} updateOnSdkTimedout={true} updateOnSdkUpdate={true} >
133132
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
134133
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
@@ -152,7 +151,7 @@ describe('SplitClient', () => {
152151
return null;
153152
}}
154153
</SplitClient>
155-
</SplitFactory>
154+
</SplitFactoryProvider>
156155
);
157156

158157
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
@@ -172,7 +171,7 @@ describe('SplitClient', () => {
172171
let previousLastUpdate = -1;
173172

174173
render(
175-
<SplitFactory factory={outerFactory} >
174+
<SplitFactoryProvider factory={outerFactory} >
176175
<SplitClient splitKey='user2' >
177176
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
178177
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
@@ -193,7 +192,7 @@ describe('SplitClient', () => {
193192
return null;
194193
}}
195194
</SplitClient>
196-
</SplitFactory>
195+
</SplitFactoryProvider>
197196
);
198197

199198
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
@@ -207,7 +206,7 @@ describe('SplitClient', () => {
207206
let count = 0;
208207

209208
render(
210-
<SplitFactory factory={outerFactory} >
209+
<SplitFactoryProvider factory={outerFactory} >
211210
<SplitClient splitKey='some_user' >
212211
{({ client }) => {
213212
count++;
@@ -221,7 +220,7 @@ describe('SplitClient', () => {
221220
return null;
222221
}}
223222
</SplitClient>
224-
</SplitFactory>
223+
</SplitFactoryProvider>
225224
);
226225

227226
expect(count).toEqual(2);
@@ -246,26 +245,27 @@ describe('SplitClient', () => {
246245
};
247246

248247
render(
249-
<SplitFactory factory={outerFactory} >
248+
<SplitFactoryProvider factory={outerFactory} >
250249
<SplitClient splitKey='user2' >
251250
<Component />
252251
</SplitClient>
253-
</SplitFactory>
252+
</SplitFactoryProvider>
254253
);
255254
});
256255

257-
test('logs error and passes null client if rendered outside an SplitProvider component.', () => {
258-
const errorSpy = jest.spyOn(console, 'error');
259-
render(
260-
<SplitClient splitKey='user2' >
261-
{({ client }) => {
262-
expect(client).toBe(null);
263-
return null;
264-
}}
265-
</SplitClient>
266-
);
267-
expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY);
268-
});
256+
// @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.
257+
// test('logs error and passes null client if rendered outside an SplitProvider component.', () => {
258+
// const errorSpy = jest.spyOn(console, 'error');
259+
// render(
260+
// <SplitClient splitKey='user2' >
261+
// {({ client }) => {
262+
// expect(client).toBe(null);
263+
// return null;
264+
// }}
265+
// </SplitClient>
266+
// );
267+
// expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY);
268+
// });
269269

270270
test(`passes a new client if re-rendered with a different splitKey.
271271
Only updates the state if the new client triggers an event, but not the previous one.`, (done) => {
@@ -338,24 +338,24 @@ describe('SplitClient', () => {
338338
}
339339

340340
render(
341-
<SplitFactory factory={outerFactory} >
341+
<SplitFactoryProvider factory={outerFactory} >
342342
<InnerComponent />
343-
</SplitFactory>
343+
</SplitFactoryProvider>
344344
);
345345
});
346346

347347
test('attributes binding test with utility', (done) => {
348348

349349
function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) {
350350
return (
351-
<SplitFactory factory={factory} attributes={attributesFactory} >
351+
<SplitFactoryProvider factory={factory} attributes={attributesFactory} >
352352
<SplitClient splitKey={splitKey} attributes={attributesClient} trafficType='user' >
353353
{() => {
354354
testSwitch(done, splitKey);
355355
return null;
356356
}}
357357
</SplitClient>
358-
</SplitFactory>
358+
</SplitFactoryProvider>
359359
);
360360
}
361361

0 commit comments

Comments
 (0)