Skip to content

Commit 83abcc5

Browse files
Merge pull request #232 from splitio/fix_useSplitClient
Fix `useSplitClient` hook to properly respect `updateOn<Event>` options when the function is re-called
2 parents 17b4d18 + 4803370 commit 83abcc5

File tree

6 files changed

+62
-17
lines changed

6 files changed

+62
-17
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.1.1 (April 8, 2025)
2+
- Bugfix - Fixed `useSplitClient` and `useSplitTreatments` hooks to properly respect `updateOn<Event>` options. Previously, if the hooks were re-called due to a component re-render, they used the latest version of the SDK client status ignoring when `updateOn<Event>` options were set to `false` and resulting in unexpected changes in treatment values.
3+
14
2.1.0 (March 28, 2025)
25
- Added a new optional `properties` argument to the options object of the `useSplitTreatments` hook, allowing to pass a map of properties to append to the generated impressions sent to Split backend. Read more in our docs.
36
- Updated @splitsoftware/splitio package to version 11.2.0 that includes some minor updates:

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-react",
3-
"version": "2.1.0",
3+
"version": "2.1.1",
44
"description": "A React library to easily integrate and use Split JS SDK",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/__tests__/SplitTreatments.test.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,36 +200,37 @@ let renderTimes = 0;
200200
* Tests for asserting that client.getTreatmentsWithConfig and client.getTreatmentsWithConfigByFlagSets are not called unnecessarily when using SplitTreatments and useSplitTreatments.
201201
*/
202202
describe.each([
203-
({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => (
203+
({ names, flagSets, attributes, updateOnSdkUpdate }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes, updateOnSdkUpdate?: boolean }) => (
204204
// @ts-expect-error names and flagSets are mutually exclusive
205-
<SplitTreatments names={names} attributes={attributes} flagSets={flagSets} >
205+
<SplitTreatments names={names} attributes={attributes} flagSets={flagSets} updateOnSdkUpdate={updateOnSdkUpdate} >
206206
{() => {
207207
renderTimes++;
208208
return null;
209209
}}
210210
</SplitTreatments>
211211
),
212-
({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => {
212+
({ names, flagSets, attributes, updateOnSdkUpdate }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes, updateOnSdkUpdate?: boolean }) => {
213213
// @ts-expect-error names and flagSets are mutually exclusive
214-
useSplitTreatments({ names, flagSets, attributes });
214+
useSplitTreatments({ names, flagSets, attributes, updateOnSdkUpdate });
215215
renderTimes++;
216216
return null;
217217
}
218218
])('SplitTreatments & useSplitTreatments optimization', (InnerComponent) => {
219219
let outerFactory = SplitFactory(sdkBrowser);
220220
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
221221

222-
function Component({ names, flagSets, attributes, splitKey, clientAttributes }: {
222+
function Component({ names, flagSets, attributes, splitKey, clientAttributes, updateOnSdkUpdate }: {
223223
names?: ISplitTreatmentsProps['names']
224224
flagSets?: ISplitTreatmentsProps['flagSets']
225225
attributes: ISplitTreatmentsProps['attributes']
226226
splitKey: ISplitClientProps['splitKey']
227-
clientAttributes?: ISplitClientProps['attributes']
227+
clientAttributes?: ISplitClientProps['attributes'],
228+
updateOnSdkUpdate?: boolean
228229
}) {
229230
return (
230231
<SplitFactoryProvider factory={outerFactory} >
231232
<SplitClient splitKey={splitKey} attributes={clientAttributes} >
232-
<InnerComponent names={names} attributes={attributes} flagSets={flagSets} />
233+
<InnerComponent names={names} attributes={attributes} flagSets={flagSets} updateOnSdkUpdate={updateOnSdkUpdate} />
233234
</SplitClient>
234235
</SplitFactoryProvider>
235236
);
@@ -322,6 +323,17 @@ describe.each([
322323
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
323324
});
324325

326+
it('rerenders and does not re-evaluate feature flags if lastUpdate timestamp does not change (e.g., SDK_UPDATE event but `updateOnSdkUpdate` false).', () => {
327+
wrapper.rerender(<Component names={names} flagSets={flagSets} attributes={attributes} splitKey={splitKey} updateOnSdkUpdate={false} />);
328+
expect(renderTimes).toBe(2);
329+
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1);
330+
331+
// SDK_UPDATE doesn't re-evaluate due to updateOnSdkUpdate false
332+
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
333+
expect(renderTimes).toBe(3);
334+
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1);
335+
});
336+
325337
it('rerenders and re-evaluates feature flags if client changes.', async () => {
326338
wrapper.rerender(<Component names={names} attributes={attributes} splitKey={'otherKey'} />);
327339
await act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY));

src/__tests__/useSplitClient.test.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { act, render } from '@testing-library/react';
2+
import { act, fireEvent, render } from '@testing-library/react';
33

44
/** Mocks */
55
import { mockSdk, Event } from './testUtils/mockSplitFactory';
@@ -87,8 +87,9 @@ describe('useSplitClient', () => {
8787

8888
let countSplitContext = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0;
8989
let countUseSplitClientWithoutUpdate = 0, countUseSplitClientUser2WithoutTimeout = 0;
90+
let previousLastUpdate = -1;
9091

91-
render(
92+
const { getByTestId } = render(
9293
<SplitFactoryProvider factory={outerFactory} >
9394
<>
9495
<SplitContext.Consumer>
@@ -129,9 +130,35 @@ describe('useSplitClient', () => {
129130
return null;
130131
})}
131132
{React.createElement(() => {
132-
useSplitClient({ splitKey: sdkBrowser.core.key, updateOnSdkUpdate: false }).client;
133+
const [state, setState] = React.useState(false);
134+
135+
const { isReady, isReadyFromCache, hasTimedout, lastUpdate } = useSplitClient({ splitKey: sdkBrowser.core.key, updateOnSdkUpdate: false });
133136
countUseSplitClientWithoutUpdate++;
134-
return null;
137+
switch (countUseSplitClientWithoutUpdate) {
138+
case 1: // initial render
139+
expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, false, false]);
140+
expect(lastUpdate).toBe(0);
141+
break;
142+
case 2: // SDK_READY_FROM_CACHE
143+
expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, false]);
144+
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
145+
break;
146+
case 3: // SDK_READY
147+
expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, false]);
148+
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
149+
break;
150+
case 4: // Forced re-render, lastUpdate doesn't change after SDK_UPDATE due to updateOnSdkUpdate = false
151+
expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, false]);
152+
expect(lastUpdate).toBe(previousLastUpdate);
153+
break;
154+
default:
155+
throw new Error('Unexpected render');
156+
}
157+
158+
previousLastUpdate = lastUpdate;
159+
return (
160+
<button data-testid="update-button" onClick={() => setState(!state)}>Force Update</button>
161+
);
135162
})}
136163
{React.createElement(() => {
137164
useSplitClient({ splitKey: 'user_2', updateOnSdkTimedout: false });
@@ -149,6 +176,7 @@ describe('useSplitClient', () => {
149176
act(() => user2Client.__emitter__.emit(Event.SDK_READY));
150177
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
151178
act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
179+
act(() => fireEvent.click(getByTestId('update-button')));
152180

153181
// SplitFactoryProvider renders once
154182
expect(countSplitContext).toEqual(1);
@@ -160,7 +188,7 @@ describe('useSplitClient', () => {
160188
expect(countUseSplitClientUser2).toEqual(5);
161189

162190
// If useSplitClient retrieves the main client and have updateOnSdkUpdate = false, it doesn't render when the main client updates.
163-
expect(countUseSplitClientWithoutUpdate).toEqual(3);
191+
expect(countUseSplitClientWithoutUpdate).toEqual(4);
164192

165193
// If useSplitClient retrieves a different client and have updateOnSdkTimedout = false, it doesn't render when the the new client times out.
166194
expect(countUseSplitClientUser2WithoutTimeout).toEqual(4);

src/useSplitClient.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextV
3737

3838
initAttributes(client, attributes);
3939

40-
const status = getStatus(client);
41-
const [, setLastUpdate] = React.useState(status.lastUpdate);
40+
const [lastUpdate, setLastUpdate] = React.useState(0);
41+
// `getStatus` is not pure. Its result depends on `client` and `lastUpdate`
42+
// eslint-disable-next-line react-hooks/exhaustive-deps
43+
const status = React.useMemo(() => getStatus(client), [client, lastUpdate]);
4244

4345
// Handle client events
4446
React.useEffect(() => {

0 commit comments

Comments
 (0)