Skip to content

Commit 6e94a5b

Browse files
Merge pull request #154 from splitio/hooks_polishing
Hooks evaluation optimization
2 parents 1eed73d + 6d8bd73 commit 6e94a5b

11 files changed

Lines changed: 80 additions & 50 deletions

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
1.10.0 (September XX, 2023)
22
- Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index. For example, `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react';` (Related to issue https://github.com/splitio/react-client/issues/162).
3+
- Updated the `useTreatments` hook to optimize feature flag evaluation. It now uses the `useMemo` hook to memoize calls to the SDK's `getTreatmentsWithConfig` function. This avoids re-evaluating feature flags when the hook is called with the same parameters and the feature flag definitions have not changed.
34
- Updated linter and other dependencies for vulnerability fixes.
45
- Bugfixing - To adhere to the rules of hooks and prevent React warnings, conditional code within hooks was removed. Previously, this code checked for the availability of the hooks API (available in React version 16.8.0 or above) and logged an error message. Now, using hooks with React versions below 16.8.0 will throw an error.
56
- Bugfixing - Updated `useClient` and `useTreatments` hooks to re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event).

src/SplitTreatments.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
11
import React from 'react';
2-
import memoizeOne from 'memoize-one';
3-
import shallowEqual from 'shallowequal';
42
import { SplitContext } from './SplitContext';
53
import { ISplitTreatmentsProps, ISplitContextValues } from './types';
64
import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants';
7-
8-
function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
9-
return newArgs[0] === lastArgs[0] && // client
10-
newArgs[1] === lastArgs[1] && // lastUpdate
11-
shallowEqual(newArgs[2], lastArgs[2]) && // names
12-
shallowEqual(newArgs[3], lastArgs[3]) && // attributes
13-
shallowEqual(newArgs[4], lastArgs[4]); // client attributes
14-
}
15-
16-
function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) {
17-
return client.getTreatmentsWithConfig(names, attributes);
18-
}
5+
import { memoizeGetTreatmentsWithConfig } from './utils';
196

207
/**
218
* SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to
@@ -27,9 +14,8 @@ export class SplitTreatments extends React.Component<ISplitTreatmentsProps> {
2714

2815
private logWarning?: boolean;
2916

30-
// Attaching a memoized `client.getTreatmentsWithConfig` function to the component instance, to avoid duplicated impressions because
31-
// the function result is the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`.
32-
private evaluateFeatureFlags = memoizeOne(evaluateFeatureFlags, argsAreEqual);
17+
// Using a memoized `client.getTreatmentsWithConfig` function to avoid duplicated impressions
18+
private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig();
3319

3420
render() {
3521
const { names, children, attributes } = this.props;

src/__tests__/SplitTreatments.test.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jest.mock('../constants', () => {
2525
import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from '../constants';
2626
import { getStatus } from '../utils';
2727
import { newSplitFactoryLocalhostInstance } from './testUtils/utils';
28+
import { useSplitTreatments } from '../useSplitTreatments';
2829

2930
describe('SplitTreatments', () => {
3031

@@ -143,13 +144,26 @@ describe('SplitTreatments', () => {
143144

144145
});
145146

147+
let renderTimes = 0;
148+
146149
/**
147-
* Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarely
150+
* Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments.
148151
*/
149-
describe('SplitTreatments optimization', () => {
150-
151-
let renderTimes = 0;
152-
152+
describe.each([
153+
({ names, attributes }) => (
154+
<SplitTreatments names={names} attributes={attributes} >
155+
{() => {
156+
renderTimes++;
157+
return null;
158+
}}
159+
</SplitTreatments>
160+
),
161+
({ names, attributes }) => {
162+
useSplitTreatments(names, attributes);
163+
renderTimes++;
164+
return null;
165+
}
166+
])('SplitTreatments & useSplitTreatments optimization', (InnerComponent) => {
153167
let outerFactory = SplitSdk(sdkBrowser);
154168
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
155169

@@ -162,12 +176,7 @@ describe('SplitTreatments optimization', () => {
162176
return (
163177
<SplitFactory factory={outerFactory} >
164178
<SplitClient splitKey={splitKey} updateOnSdkUpdate={true} attributes={clientAttributes} >
165-
<SplitTreatments names={names} attributes={attributes} >
166-
{() => {
167-
renderTimes++;
168-
return null;
169-
}}
170-
</SplitTreatments>
179+
<InnerComponent names={names} attributes={attributes} />
171180
</SplitClient>
172181
</SplitFactory>
173182
);
@@ -243,17 +252,17 @@ describe('SplitTreatments optimization', () => {
243252
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
244253
});
245254

246-
it('rerenders and re-evaluates feature flags if client changes.', () => {
255+
it('rerenders and re-evaluates feature flags if client changes.', async () => {
247256
wrapper.rerender(<Component names={names} attributes={attributes} splitKey={'otherKey'} />);
248-
act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY));
257+
await act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY));
249258

250259
// Initial render + 2 renders (in 3 updates) -> automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
251260
expect(renderTimes).toBe(3);
252261
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1);
253262
expect(outerFactory.client('otherKey').getTreatmentsWithConfig).toBeCalledTimes(1);
254263
});
255264

256-
it('rerenders and re-evaluate splfeature flagsits when Split context changes (in both SplitFactory and SplitClient components).', async () => {
265+
it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => {
257266
// changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments
258267
const outerFactory = SplitSdk(sdkBrowser);
259268
const names = ['split1', 'split2'];

src/__tests__/useSplitClient.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ test('useSplitClient must update on SDK events', () => {
7474
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
7575
{React.createElement(() => {
7676
const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true });
77-
countNestedComponent++;
78-
7977
expect(status.client).toBe(user2Client);
78+
79+
// useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event
80+
countNestedComponent++;
8081
switch (countNestedComponent) {
8182
case 1:
8283
expect(status.isReady).toBe(false);

src/__tests__/useSplitTreatments.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTre
3434
}
3535
}
3636

37-
test('useSplitTreatments', async () => {
37+
test('useSplitTreatments must update on SDK events', async () => {
3838
const outerFactory = SplitSdk(sdkBrowser);
3939
const mainClient = outerFactory.client() as any;
4040
const user2Client = outerFactory.client('user_2') as any;

src/__tests__/useTrack.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ describe('useTrack', () => {
2121
const value = 10;
2222
const properties = { prop1: 'prop1' };
2323

24-
test('returns the track method binded to the client at Split context updated by SplitFactory.', () => {
24+
test('returns the track method bound to the client at Split context updated by SplitFactory.', () => {
2525
const outerFactory = SplitSdk(sdkBrowser);
26-
let bindedTrack;
26+
let boundTrack;
2727
let trackResult;
2828

2929
render(
3030
<SplitFactory factory={outerFactory} >
3131
{React.createElement(() => {
32-
bindedTrack = useTrack();
33-
trackResult = bindedTrack(tt, eventType, value, properties);
32+
boundTrack = useTrack();
33+
trackResult = boundTrack(tt, eventType, value, properties);
3434
return null;
3535
})}
3636
</SplitFactory>,
@@ -40,17 +40,17 @@ describe('useTrack', () => {
4040
expect(track).toHaveReturnedWith(trackResult);
4141
});
4242

43-
test('returns the track method binded to the client at Split context updated by SplitClient.', () => {
43+
test('returns the track method bound to the client at Split context updated by SplitClient.', () => {
4444
const outerFactory = SplitSdk(sdkBrowser);
45-
let bindedTrack;
45+
let boundTrack;
4646
let trackResult;
4747

4848
render(
4949
<SplitFactory factory={outerFactory} >
5050
<SplitClient splitKey='user2' >
5151
{React.createElement(() => {
52-
bindedTrack = useTrack();
53-
trackResult = bindedTrack(tt, eventType, value, properties);
52+
boundTrack = useTrack();
53+
trackResult = boundTrack(tt, eventType, value, properties);
5454
return null;
5555
})}
5656
</SplitClient>
@@ -61,16 +61,16 @@ describe('useTrack', () => {
6161
expect(track).toHaveReturnedWith(trackResult);
6262
});
6363

64-
test('returns the track method binded to a new client given a splitKey and optional trafficType.', () => {
64+
test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => {
6565
const outerFactory = SplitSdk(sdkBrowser);
66-
let bindedTrack;
66+
let boundTrack;
6767
let trackResult;
6868

6969
render(
7070
<SplitFactory factory={outerFactory} >
7171
{React.createElement(() => {
72-
bindedTrack = useTrack('user2', tt);
73-
trackResult = bindedTrack(eventType, value, properties);
72+
boundTrack = useTrack('user2', tt);
73+
trackResult = boundTrack(eventType, value, properties);
7474
return null;
7575
})}
7676
</SplitFactory>,

src/useSplitTreatments.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import React from 'react';
12
import { getControlTreatmentsWithConfig } from './constants';
2-
import { IClientWithContext } from './utils';
3+
import { IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils';
34
import { ISplitTreatmentsChildProps, IUpdateProps } from './types';
45
import { useSplitClient } from './useSplitClient';
56

@@ -13,8 +14,11 @@ import { useSplitClient } from './useSplitClient';
1314
export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps {
1415
const context = useSplitClient(key, undefined, undefined, options);
1516
const client = context.client;
17+
18+
const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []);
19+
1620
const treatments = client && (client as IClientWithContext).__getStatus().isOperational ?
17-
client.getTreatmentsWithConfig(splitNames, attributes) :
21+
getTreatmentsWithConfig(client, context.lastUpdate, splitNames, attributes, { ...client.getAttributes() }) :
1822
getControlTreatmentsWithConfig(splitNames);
1923

2024
return {

src/useTrack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const noOpFalse = () => false;
77
* 'useTrack' is a hook that returns the track method from a Split client.
88
* It uses the 'useContext' hook to access the client from the Split context.
99
*
10-
* @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false.
10+
* @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false.
1111
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track}
1212
*/
1313
export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] {

src/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import memoizeOne from 'memoize-one';
2+
import shallowEqual from 'shallowequal';
13
import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client';
24
import { VERSION } from './constants';
35
import { ISplitStatus } from './types';
@@ -160,3 +162,23 @@ function uniq(arr: string[]): string[] {
160162
function isString(val: unknown): val is string {
161163
return typeof val === 'string' || val instanceof String;
162164
}
165+
166+
/**
167+
* Gets a memoized version of the `client.getTreatmentsWithConfig` method.
168+
* It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`.
169+
*/
170+
export function memoizeGetTreatmentsWithConfig() {
171+
return memoizeOne(evaluateFeatureFlags, argsAreEqual);
172+
}
173+
174+
function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
175+
return newArgs[0] === lastArgs[0] && // client
176+
newArgs[1] === lastArgs[1] && // lastUpdate
177+
shallowEqual(newArgs[2], lastArgs[2]) && // names
178+
shallowEqual(newArgs[3], lastArgs[3]) && // attributes
179+
shallowEqual(newArgs[4], lastArgs[4]); // client attributes
180+
}
181+
182+
function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) {
183+
return client.getTreatmentsWithConfig(names, attributes);
184+
}

types/useTrack.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* 'useTrack' is a hook that returns the track method from a Split client.
33
* It uses the 'useContext' hook to access the client from the Split context.
44
*
5-
* @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false.
5+
* @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false.
66
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track}
77
*/
88
export declare function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'];

0 commit comments

Comments
 (0)