Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SplitFactoryProvider component and deprecate SplitFactory component and withSplitFactory HOC #182

Merged
merged 21 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
329b52b
Add new SplitFactoryProvider component
EmilianoSanchez Jan 10, 2024
1dae653
Update comments
EmilianoSanchez Jan 10, 2024
8bb224b
Update unit tests to use the new SplitFactoryProvider
EmilianoSanchez Jan 10, 2024
0410c0c
Add unit test for the new SplitFactoryProvider
EmilianoSanchez Jan 10, 2024
68dd9fa
Polishing
EmilianoSanchez Jan 11, 2024
cf1f636
Add CHANGES entry
EmilianoSanchez Jan 11, 2024
6bf6b3c
Prepare rc
EmilianoSanchez Jan 11, 2024
1c0b65a
Polishing
EmilianoSanchez Jan 11, 2024
326aa6d
Add new SplitFactoryProvider component and deprecate SplitFactory com…
EmilianoSanchez Jan 11, 2024
c59fe5e
Fix logs for new SplitFactoryProvider component
EmilianoSanchez Jan 11, 2024
528be3e
Merge branch 'fix_logs' into add_SplitFactoryProvider
EmilianoSanchez Jan 11, 2024
068f2a2
Rename ci.yml to ci-cd.yml
EmilianoSanchez Jan 11, 2024
9052984
Merge branch 'fix_logs' into add_SplitFactoryProvider
EmilianoSanchez Jan 11, 2024
53a0a90
Update changelog entry
EmilianoSanchez Jan 11, 2024
0b869f9
Update deprecation comment to add more info
EmilianoSanchez Jan 11, 2024
561d83d
Component prop update test
EmilianoSanchez Jan 15, 2024
1504d59
Optimize SplitFactoryProvider: do not recreate factory if only update…
EmilianoSanchez Jan 16, 2024
045c835
Merge pull request #181 from splitio/add_SplitFactoryProvider
EmilianoSanchez Jan 16, 2024
d50d900
Merge pull request #183 from splitio/fix_logs
EmilianoSanchez Jan 16, 2024
8af925c
Update comments and CHANGES file
EmilianoSanchez Jan 16, 2024
b56d33d
Prepare stable version
EmilianoSanchez Jan 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml → .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16.16.0
lts/*
7 changes: 7 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
1.11.0 (January 16, 2023)
- Added the new `SplitFactoryProvider` component as a replacement for the now deprecated `SplitFactory` component.
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).
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).
- Updated internal code to remove a circular dependency and avoid warning messages with tools like PNPM (Related to issue #176).
- Updated @splitsoftware/splitio package to version 10.25.1 for vulnerability fixes.

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.

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -48,10 +48,10 @@ function MyComponent() {

function MyApp() {
return (
// Use SplitFactory to instantiate the SDK and makes it available to nested components
<SplitFactory config={CONFIG} >
// Use SplitFactoryProvider to instantiate the SDK and makes it available to nested components
<SplitFactoryProvider config={CONFIG} >
<MyComponent />
</SplitFactory>
</SplitFactoryProvider>
);
}
```
Expand Down
50 changes: 25 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-react",
"version": "1.10.2",
"version": "1.11.0",
"description": "A React library to easily integrate and use Split JS SDK",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down Expand Up @@ -62,7 +62,7 @@
},
"homepage": "https://github.com/splitio/react-client#readme",
"dependencies": {
"@splitsoftware/splitio": "10.24.1",
"@splitsoftware/splitio": "10.25.1",
"memoize-one": "^5.1.1",
"shallowequal": "^1.1.0"
},
Expand Down
13 changes: 3 additions & 10 deletions src/SplitClient.tsx
Original file line number Diff line number Diff line change
@@ -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<IUpdateProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes, children: any }, ISplitContextValues> {

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

// Log error if factory is not available
if (!factory) {
console.error(ERROR_SC_NO_FACTORY);
}

this.state = {
factory,
client,
Expand Down Expand Up @@ -129,9 +123,8 @@ export class SplitComponent extends React.Component<IUpdateProps & { factory: Sp
* SplitClient will initialize a new SDK client and listen for its events in order to update the Split Context.
* Children components will have access to the new client when accessing Split Context.
*
* Unlike SplitFactory, the underlying SDK client can be changed during the component lifecycle
* if the component is updated with a different splitKey or trafficType prop. Since the client can change,
* its release is not handled by SplitClient but by its container SplitFactory component.
* The underlying SDK client can be changed during the component lifecycle
* if the component is updated with a different splitKey or trafficType prop.
*
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
*/
Expand Down
2 changes: 1 addition & 1 deletion src/SplitContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export const INITIAL_CONTEXT: ISplitContextValues = {
/**
* Split Context is the React Context instance that represents our SplitIO global state.
* It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate)
* The context is created with default empty values, that eventually SplitFactory and SplitClient access and update.
* The context is created with default empty values, that SplitFactoryProvider and SplitClient access and update.
*/
export const SplitContext = React.createContext<ISplitContextValues>(INITIAL_CONTEXT);
9 changes: 7 additions & 2 deletions src/SplitFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
/**
* SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context,
* and automatically shutdown and release resources when it is unmounted. SplitFactory must wrap other components and functions
* from this library, since they access the Split Context and its elements (factory, clients, etc).
* from this library, since they access the Split Context and its properties (factory, client, isReady, etc).
*
* The underlying SDK factory and client is set on the constructor, and cannot be changed during the component lifecycle,
* 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.
* `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.
* 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
* 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.
*
* @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client}
*/
export class SplitFactory extends React.Component<ISplitFactoryProps, { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }> {

Expand Down
86 changes: 86 additions & 0 deletions src/SplitFactoryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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, __factories } from './utils';
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';

/**
* 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,
* and automatically destroy the SDK (shutdown and release resources) when it is unmounted or `config` prop updated. SplitFactoryProvider must wrap other library components and
* functions since they access the Split Context and its properties (factory, client, isReady, 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 [configFactory, setConfigFactory] = React.useState<IFactoryWithClients | null>(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);

// 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);
}

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 unsubscribe;
}
}, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]);

return (
<SplitComponent {...props} factory={factory} client={client} />
);
}
8 changes: 1 addition & 7 deletions src/SplitTreatments.tsx
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -26,7 +25,7 @@ export class SplitTreatments extends React.Component<ISplitTreatmentsProps> {
{(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,
Expand All @@ -35,9 +34,4 @@ export class SplitTreatments extends React.Component<ISplitTreatmentsProps> {
</SplitContext.Consumer>
);
}

componentDidMount() {
if (this.logWarning) { console.log(WARN_ST_NO_CLIENT); }
}

}
Loading