Skip to content

Commit 94dbb33

Browse files
Merge pull request #252 from splitio/fallback-treatment-support
Add support for fallback treatments
2 parents 571bec2 + 596cf92 commit 94dbb33

14 files changed

+160
-62
lines changed

CHANGES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
2.6.0 (October 31, 2025)
1+
2.6.0 (November 4, 2025)
22
- Added `useTreatment`, `useTreatments`, `useTreatmentWithConfig` and `useTreatmentsWithConfig` hooks to replace the now deprecated `useSplitTreatments` hook.
33
- Updated @splitsoftware/splitio package to version 11.8.0 that includes minor updates:
44
- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.

MIGRATION-GUIDE.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,32 @@
33

44
React SDK v2.0.0 has a few breaking changes that you should consider when migrating from a previous version. The main changes are:
55

6-
### • Deprecated `useClient`, `useTreatments`, and `useManager` hooks have been removed.
76

8-
Follow [this section](#migrating-to-get-react-sdk-v1100-improvements-replacing-the-deprecated-useclient-usetreatments-and-usemanager-hooks) to migrate to the new hooks `useSplitClient`, `useSplitTreatments`, and `useSplitManager`.
7+
### `useTreatments` hook was removed in v2.0.0, but re-introduced in v2.6.0 with a different API:
8+
9+
Since v2.6.0, there are 4 hooks variants to evaluate feature flags, to better cover the different evaluation methods available in the JavaScript SDK client:
10+
11+
- `useTreatment`: returns a treatment value for a given feature flag name. It calls `client.getTreatment()` method under the hood.
12+
- `useTreatmentWithConfig`: returns a single treatment value and its configuration for a given feature flag name. It calls `client.getTreatmentWithConfig()` method under the hood.
13+
- `useTreatments`: returns an object with treatment values for multiple feature flag names. It calls `client.getTreatments()` or `client.getTreatmentsByFlagSets()` methods under the hood, depending if the `names` or `flagSets` option is provided.
14+
- `useTreatmentsWithConfig`: returns an object with treatment values and their configurations for multiple feature flag names. It calls `client.getTreatmentsWithConfig()` or `client.getTreatmentsWithConfigByFlagSets()` methods under the hood, depending if the `names` or `flagSets` option is provided.
15+
16+
The `useTreatments` hook from v1.x.x should be replaced with `useTreatmentsWithConfig`, as follows:
17+
18+
```javascript
19+
// v1.x.x
20+
const treatments = useTreatments(featureFlagNames, optionalAttributes, optionalSplitKey);
21+
22+
// v2.6.0+
23+
const { treatments } = useTreatmentsWithConfig({ names: featureFlagNames, attributes: optionalAttributes, splitKey: optionalSplitKey });
24+
25+
// v2.0.0-v2.5.0
26+
const { treatments } = useSplitTreatments({ names: featureFlagNames, attributes: optionalAttributes, splitKey: optionalSplitKey });
27+
```
28+
29+
### • Deprecated `useClient` and `useManager` hooks have been removed.
30+
31+
Follow [this section](#migrating-to-get-react-sdk-v1100-improvements-replacing-the-deprecated-useclient-usetreatments-and-usemanager-hooks) to migrate to the new hooks `useSplitClient` and `useSplitManager`.
932

1033
### • Updated the default value of `updateOnSdkUpdate` and `updateOnSdkTimedout` options to `true`.
1134

@@ -15,7 +38,7 @@ Consider setting the `updateOnSdkUpdate` option to `false` to revert to the prev
1538

1639
The same applies for the equivalent props in the `[with]SplitClient` and `[with]SplitTreatments` components, although these components are deprecated and we recommend [migrating to their hook alternatives](#-high-order-components-withsplitclient-withsplittreatments-and-components-that-accept-a-render-function-as-child-component-splittreatments-and-splitclient-have-been-deprecated-and-might-be-removed-in-a-future-major-release).
1740

18-
### • Deprecated `SplitFactory` provider has been removed, `withSplitFactory` is deprecated, and `SplitFactoryProvider` doesn't accept `updateOn` props and a render function as children anymore.
41+
### • Deprecated `SplitFactory` provider has been removed, `withSplitFactory` is deprecated, and `SplitFactoryProvider` doesn't accept a render function as children anymore.
1942

2043
To migrate your existing code to the new version of `SplitFactoryProvider`, consider the following refactor example:
2144

@@ -53,21 +76,21 @@ should be refactored to:
5376

5477
```tsx
5578
const MyComponent = () => {
56-
const props: ISplitContextValues = useSplitClient({ updateOnSdkUpdate: false });
79+
const props: ISplitContextValues = useSplitClient();
5780
const { factory, client, isReady, isReadyFromCache, ... } = props;
5881
...
5982
};
6083

6184
const App = () => {
6285
return (
63-
<SplitFactoryProvider config={mySplitConfig} attributes={DEFAULT_CLIENT_ATTRIBUTES} >
86+
<SplitFactoryProvider config={mySplitConfig} updateOnSdkUpdate={false} attributes={DEFAULT_CLIENT_ATTRIBUTES} >
6487
<MyComponent />
6588
</SplitFactoryProvider>
6689
);
6790
};
6891
```
6992

70-
Notice that `MyComponent` was refactored to use the `useSplitClient` hook and is passed as a React JSX element rather than a render function. The `useSplitClient` hook is called without providing a `splitKey` param. This means that the default client (whose key is set in the `core.key` property of the `mySplitConfig` object) will be used, and the `updateOnSdkUpdate` and `attributes` props are passed as options to the hook.
93+
Notice that `MyComponent` was refactored to use the `useSplitClient` hook and is passed as a React JSX element rather than a render function. The `useSplitClient` hook is called without providing a `splitKey` param. This means that the default client (whose key is set in the `core.key` property of the `mySplitConfig` object) will be used.
7194

7295
### • High-Order-Components (`withSplitClient`, `withSplitTreatments`) and components that accept a render function as child component (`SplitTreatments`, and `SplitClient`) have been deprecated and might be removed in a future major release.
7396

package-lock.json

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-react",
3-
"version": "2.5.0",
3+
"version": "2.6.0",
44
"description": "A React library to easily integrate and use Split JS SDK",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",
@@ -63,7 +63,7 @@
6363
},
6464
"homepage": "https://github.com/splitio/react-client#readme",
6565
"dependencies": {
66-
"@splitsoftware/splitio": "11.7.2-rc.4",
66+
"@splitsoftware/splitio": "11.8.0",
6767
"memoize-one": "^5.1.1",
6868
"shallowequal": "^1.1.0",
6969
"tslib": "^2.3.1"

src/__tests__/testUtils/sdkConfigs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ export const sdkBrowser: SplitIO.IBrowserSettings = {
44
key: 'customer-key',
55
},
66
};
7+
8+
export const sdkBrowserWithConfig: SplitIO.IBrowserSettings = {
9+
...sdkBrowser,
10+
fallbackTreatments: {
11+
global: 'control_global',
12+
byFlag: { ff1: { treatment: 'control_ff1', config: 'control_ff1_config' } }
13+
}
14+
};

src/__tests__/useTreatment.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jest.mock('@splitsoftware/splitio/client', () => {
77
return { SplitFactory: mockSdk() };
88
});
99
import { SplitFactory } from '@splitsoftware/splitio/client';
10-
import { sdkBrowser } from './testUtils/sdkConfigs';
10+
import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
1111
import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
1212

1313
/** Test target */
@@ -96,7 +96,7 @@ describe('useTreatment', () => {
9696
}).toThrow(EXCEPTION_NO_SFP);
9797
});
9898

99-
test('useTreatment must update on SDK events', async () => {
99+
test('must update on SDK events', async () => {
100100
const outerFactory = SplitFactory(sdkBrowser);
101101
const mainClient = outerFactory.client() as any;
102102
const user2Client = outerFactory.client('user_2') as any;
@@ -171,4 +171,16 @@ describe('useTreatment', () => {
171171
expect(user2Client.getTreatment).toHaveBeenLastCalledWith('split_test', undefined, undefined);
172172
});
173173

174+
test('returns fallback treatment if the client is not operational', () => {
175+
render(
176+
<SplitFactoryProvider config={sdkBrowserWithConfig} >
177+
{React.createElement(() => {
178+
expect(useTreatment({ name: featureFlagName, attributes, properties }).treatment).toEqual('control_global');
179+
expect(useTreatment({ name: 'ff1', attributes, properties }).treatment).toEqual('control_ff1');
180+
return null;
181+
})}
182+
</SplitFactoryProvider>
183+
);
184+
});
185+
174186
});

src/__tests__/useTreatmentWithConfig.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jest.mock('@splitsoftware/splitio/client', () => {
77
return { SplitFactory: mockSdk() };
88
});
99
import { SplitFactory } from '@splitsoftware/splitio/client';
10-
import { sdkBrowser } from './testUtils/sdkConfigs';
10+
import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
1111
import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
1212

1313
/** Test target */
@@ -96,7 +96,7 @@ describe('useTreatmentWithConfig', () => {
9696
}).toThrow(EXCEPTION_NO_SFP);
9797
});
9898

99-
test('useTreatmentWithConfig must update on SDK events', async () => {
99+
test('must update on SDK events', async () => {
100100
const outerFactory = SplitFactory(sdkBrowser);
101101
const mainClient = outerFactory.client() as any;
102102
const user2Client = outerFactory.client('user_2') as any;
@@ -171,4 +171,16 @@ describe('useTreatmentWithConfig', () => {
171171
expect(user2Client.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', undefined, undefined);
172172
});
173173

174+
test('returns fallback treatment if the client is not operational', () => {
175+
render(
176+
<SplitFactoryProvider config={sdkBrowserWithConfig} >
177+
{React.createElement(() => {
178+
expect(useTreatmentWithConfig({ name: featureFlagName, attributes, properties }).treatment).toEqual({ treatment: 'control_global', config: null });
179+
expect(useTreatmentWithConfig({ name: 'ff1', attributes, properties }).treatment).toEqual({ treatment: 'control_ff1', config: 'control_ff1_config' });
180+
return null;
181+
})}
182+
</SplitFactoryProvider>
183+
);
184+
});
185+
174186
});

src/__tests__/useTreatments.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jest.mock('@splitsoftware/splitio/client', () => {
77
return { SplitFactory: mockSdk() };
88
});
99
import { SplitFactory } from '@splitsoftware/splitio/client';
10-
import { sdkBrowser } from './testUtils/sdkConfigs';
10+
import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
1111
import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
1212

1313
/** Test target */
@@ -132,7 +132,7 @@ describe('useTreatments', () => {
132132
);
133133
});
134134

135-
test('useTreatments must update on SDK events', async () => {
135+
test('must update on SDK events', async () => {
136136
const outerFactory = SplitFactory(sdkBrowser);
137137
const mainClient = outerFactory.client() as any;
138138
const user2Client = outerFactory.client('user_2') as any;
@@ -226,4 +226,16 @@ describe('useTreatments', () => {
226226
);
227227
});
228228

229+
test('returns fallback treatments if the client is not operational', () => {
230+
render(
231+
<SplitFactoryProvider config={sdkBrowserWithConfig} >
232+
{React.createElement(() => {
233+
const { treatments } = useTreatments({ names: ['ff1', 'ff2'], attributes, properties });
234+
expect(treatments).toEqual({ ff1: 'control_ff1', ff2: 'control_global' });
235+
return null;
236+
})}
237+
</SplitFactoryProvider>
238+
);
239+
});
240+
229241
});

src/__tests__/useTreatmentsWithConfig.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jest.mock('@splitsoftware/splitio/client', () => {
77
return { SplitFactory: mockSdk() };
88
});
99
import { SplitFactory } from '@splitsoftware/splitio/client';
10-
import { sdkBrowser } from './testUtils/sdkConfigs';
10+
import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
1111
import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
1212

1313
/** Test target */
@@ -132,7 +132,7 @@ describe('useTreatmentsWithConfig', () => {
132132
);
133133
});
134134

135-
test('useTreatmentsWithConfig must update on SDK events', async () => {
135+
test('must update on SDK events', async () => {
136136
const outerFactory = SplitFactory(sdkBrowser);
137137
const mainClient = outerFactory.client() as any;
138138
const user2Client = outerFactory.client('user_2') as any;
@@ -232,4 +232,16 @@ describe('useTreatmentsWithConfig', () => {
232232
);
233233
});
234234

235+
test('returns fallback treatments if the client is not operational', () => {
236+
render(
237+
<SplitFactoryProvider config={sdkBrowserWithConfig} >
238+
{React.createElement(() => {
239+
const { treatments } = useTreatmentsWithConfig({ names: ['ff1', 'ff2'], attributes, properties });
240+
expect(treatments).toEqual({ ff1: { treatment: 'control_ff1', config: 'control_ff1_config' }, ff2: { treatment: 'control_global', config: null } });
241+
return null;
242+
})}
243+
</SplitFactoryProvider>
244+
);
245+
});
246+
235247
});

src/useTreatment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import { useSplitClient } from './useSplitClient';
1919
*/
2020
export function useTreatment(options: IUseTreatmentOptions): IUseTreatmentResult {
2121
const context = useSplitClient({ ...options, attributes: undefined });
22-
const { client, lastUpdate } = context;
22+
const { factory, client, lastUpdate } = context;
2323
const { name, attributes, properties } = options;
2424

2525
const getTreatment = React.useMemo(memoizeGetTreatment, []);
2626

2727
// Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
2828
// Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
29-
const treatment = getTreatment(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties });
29+
const treatment = getTreatment(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties }, factory);
3030

3131
return {
3232
...context,

0 commit comments

Comments
 (0)