Skip to content

Commit 5fa09ae

Browse files
Merge pull request #182 from splitio/new_SplitFactoryProvider_component
Add `SplitFactoryProvider` component and deprecate `SplitFactory` component and `withSplitFactory` HOC
2 parents 65ad71c + b56d33d commit 5fa09ae

29 files changed

+734
-183
lines changed

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

+2-2
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v16.16.0
1+
lts/*

CHANGES.txt

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
1.11.0 (January 16, 2023)
2+
- Added the new `SplitFactoryProvider` component as a replacement for the now deprecated `SplitFactory` component.
3+
The new component is a revised version of `SplitFactory`, addressing improper handling of the SDK initialization side-effects in the `componentDidMount` and `componentDidUpdate` methods (commit phase), causing some issues like memory leaks and the SDK not reinitializing when component props change (Related to issue #11 and #148).
4+
The `SplitFactoryProvider` component can be used as a drop-in replacement for `SplitFactory`. It utilizes the React Hooks API, that requires React 16.8.0 or later, and supports server-side rendering. See our documentation for more details (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+
- Updated @splitsoftware/splitio package to version 10.25.1 for vulnerability fixes.
7+
18
1.10.2 (December 12, 2023)
29
- 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.
310

README.md

+4-4
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
```

package-lock.json

+25-25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-react",
3-
"version": "1.10.2",
3+
"version": "1.11.0",
44
"description": "A React library to easily integrate and use Split JS SDK",
55
"main": "lib/index.js",
66
"module": "es/index.js",
@@ -62,7 +62,7 @@
6262
},
6363
"homepage": "https://github.com/splitio/react-client#readme",
6464
"dependencies": {
65-
"@splitsoftware/splitio": "10.24.1",
65+
"@splitsoftware/splitio": "10.25.1",
6666
"memoize-one": "^5.1.1",
6767
"shallowequal": "^1.1.0"
6868
},

src/SplitClient.tsx

+3-10
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

+1-1
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/SplitFactory.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
99
/**
1010
* SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context,
1111
* and automatically shutdown and release resources when it is unmounted. SplitFactory must wrap other components and functions
12-
* from this library, since they access the Split Context and its elements (factory, clients, etc).
12+
* from this library, since they access the Split Context and its properties (factory, client, isReady, etc).
1313
*
1414
* The underlying SDK factory and client is set on the constructor, and cannot be changed during the component lifecycle,
1515
* even if the component is updated with a different config or factory prop.
1616
*
17-
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK}
17+
* @deprecated Replace with the new `SplitFactoryProvider` component.
18+
* `SplitFactoryProvider` is a drop-in replacement that properly handles side effects (factory creation and destruction) within the React component lifecycle, avoiding issues with factory recreation and memory leaks.
19+
* Note: There is a subtle breaking change in `SplitFactoryProvider`. When using the `config` prop, `factory` and `client` properties in the context are `null` in the first render, until the context is updated when some event is emitted on
20+
* the SDK main client (ready, ready from cache, timeout or update depending on the configuration of the `updateOnXXX` props of the component). This differs from the previous behavior where `factory` and `client` were immediately available.
21+
*
22+
* @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
1823
*/
1924
export class SplitFactory extends React.Component<ISplitFactoryProps, { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }> {
2025

src/SplitFactoryProvider.tsx

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from 'react';
2+
3+
import { SplitComponent } from './SplitClient';
4+
import { ISplitFactoryProps } from './types';
5+
import { WARN_SF_CONFIG_AND_FACTORY } from './constants';
6+
import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus, __factories } from './utils';
7+
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
8+
9+
/**
10+
* SplitFactoryProvider will initialize the Split SDK and its main client when `config` prop is provided or updated, listen for its events in order to update the Split Context,
11+
* and automatically destroy the SDK (shutdown and release resources) when it is unmounted or `config` prop updated. SplitFactoryProvider must wrap other library components and
12+
* functions since they access the Split Context and its properties (factory, client, isReady, etc).
13+
*
14+
* NOTE: Either pass a factory instance or a config object. If both are passed, the config object will be ignored.
15+
* 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.
16+
*
17+
* @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
18+
*/
19+
export function SplitFactoryProvider(props: ISplitFactoryProps) {
20+
let {
21+
config, factory: propFactory,
22+
updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate
23+
} = { ...DEFAULT_UPDATE_OPTIONS, ...props };
24+
25+
if (config && propFactory) {
26+
console.log(WARN_SF_CONFIG_AND_FACTORY);
27+
config = undefined;
28+
}
29+
30+
const [configFactory, setConfigFactory] = React.useState<IFactoryWithClients | null>(null);
31+
const factory = propFactory || (configFactory && config === configFactory.config ? configFactory : null);
32+
const client = factory ? getSplitClient(factory) : null;
33+
34+
// Effect to initialize and destroy the factory
35+
React.useEffect(() => {
36+
if (config) {
37+
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) {
49+
const client = getSplitClient(factory);
50+
const status = getStatus(client);
51+
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 = () => {
59+
client.off(client.Event.SDK_READY, update);
60+
client.off(client.Event.SDK_READY_FROM_CACHE, update);
61+
client.off(client.Event.SDK_READY_TIMED_OUT, update);
62+
client.off(client.Event.SDK_UPDATE, update);
63+
}
64+
65+
if (updateOnSdkReady) {
66+
if (status.isReady) update();
67+
else client.once(client.Event.SDK_READY, update);
68+
}
69+
if (updateOnSdkReadyFromCache) {
70+
if (status.isReadyFromCache) update();
71+
else client.once(client.Event.SDK_READY_FROM_CACHE, update);
72+
}
73+
if (updateOnSdkTimedout) {
74+
if (status.hasTimedout) update();
75+
else client.once(client.Event.SDK_READY_TIMED_OUT, update);
76+
}
77+
if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update);
78+
79+
return unsubscribe;
80+
}
81+
}, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]);
82+
83+
return (
84+
<SplitComponent {...props} factory={factory} client={client} />
85+
);
86+
}

src/SplitTreatments.tsx

+1-7
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
}

0 commit comments

Comments
 (0)