Skip to content

Commit 1eed73d

Browse files
Merge pull request #152 from splitio/hooks_update_options
Added update options to hooks
2 parents 0fce4b5 + f1cc018 commit 1eed73d

6 files changed

Lines changed: 143 additions & 44 deletions

File tree

src/__tests__/useSplitClient.test.tsx

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,89 @@ import { useSplitClient } from '../useSplitClient';
1515
import { SplitClient } from '../SplitClient';
1616
import { SplitContext } from '../SplitContext';
1717

18-
test('useSplitClient', () => {
18+
test('useSplitClient must update on SDK events', () => {
1919
const outerFactory = SplitSdk(sdkBrowser);
2020
const mainClient = outerFactory.client() as any;
2121
const user2Client = outerFactory.client('user_2') as any;
2222

2323
let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0;
24+
let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0;
25+
let countNestedComponent = 0;
2426

2527
render(
2628
<SplitFactory factory={outerFactory} >
2729
<>
2830
<SplitContext.Consumer>
2931
{() => countSplitContext++}
3032
</SplitContext.Consumer>
31-
<SplitClient splitKey={sdkBrowser.core.key}>
33+
<SplitClient splitKey={sdkBrowser.core.key} trafficType={sdkBrowser.core.trafficType}
34+
/* Disabling update props is ineffective because the wrapping SplitFactory has them enabled: */
35+
updateOnSdkReady={false} updateOnSdkReadyFromCache={false}
36+
>
3237
{() => { countSplitClient++; return null }}
3338
</SplitClient>
34-
<SplitClient splitKey={'user_2'}>
35-
{() => { countSplitClientUser2++; return null }}
36-
</SplitClient>
3739
{React.createElement(() => {
38-
const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });
40+
// Equivalent to
41+
// - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });`
42+
// - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });`
43+
const { client } = useSplitClient(undefined, undefined, { att1: 'att1' });
3944
expect(client).toBe(mainClient); // Assert that the main client was retrieved.
4045
expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes.
4146
countUseSplitClient++;
4247
return null;
4348
})}
49+
<SplitClient splitKey={'user_2'}>
50+
{() => { countSplitClientUser2++; return null }}
51+
</SplitClient>
4452
{React.createElement(() => {
4553
const { client } = useSplitClient('user_2');
4654
expect(client).toBe(user2Client);
4755
countUseSplitClientUser2++;
4856
return null;
4957
})}
58+
<SplitClient splitKey={sdkBrowser.core.key} updateOnSdkUpdate={true} >
59+
{() => { countSplitClientWithUpdate++; return null }}
60+
</SplitClient>
61+
{React.createElement(() => {
62+
useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, undefined, { updateOnSdkUpdate: true }).client;
63+
countUseSplitClientWithUpdate++;
64+
return null;
65+
})}
66+
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
67+
{() => { countSplitClientUser2WithUpdate++; return null }}
68+
</SplitClient>
69+
{React.createElement(() => {
70+
useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true });
71+
countUseSplitClientUser2WithUpdate++;
72+
return null;
73+
})}
74+
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
75+
{React.createElement(() => {
76+
const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true });
77+
countNestedComponent++;
78+
79+
expect(status.client).toBe(user2Client);
80+
switch (countNestedComponent) {
81+
case 1:
82+
expect(status.isReady).toBe(false);
83+
expect(status.isReadyFromCache).toBe(false);
84+
break;
85+
case 2:
86+
expect(status.isReady).toBe(false);
87+
expect(status.isReadyFromCache).toBe(true);
88+
break;
89+
case 3:
90+
expect(status.isReady).toBe(true);
91+
expect(status.isReadyFromCache).toBe(true);
92+
break;
93+
case 4:
94+
break;
95+
default:
96+
throw new Error('Unexpected render');
97+
}
98+
return null;
99+
})}
100+
</SplitClient>
50101
</>
51102
</SplitFactory>
52103
);
@@ -70,4 +121,53 @@ test('useSplitClient', () => {
70121
// they render when the context renders and when the new client is ready and ready from cache.
71122
expect(countSplitClientUser2).toEqual(countSplitContext + 2);
72123
expect(countUseSplitClientUser2).toEqual(countSplitContext + 2);
124+
125+
// If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true,
126+
// they render when the context renders and when the client updates.
127+
expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1);
128+
expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1);
129+
130+
// If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true,
131+
// they render when the context renders and when the new client is ready, ready from cache and updates.
132+
expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3);
133+
expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3);
134+
135+
expect(countNestedComponent).toEqual(4);
136+
});
137+
138+
test('useSplitClient must support changes in update props', () => {
139+
const outerFactory = SplitSdk(sdkBrowser);
140+
const mainClient = outerFactory.client() as any;
141+
142+
let rendersCount = 0;
143+
144+
function InnerComponent(updateOptions) {
145+
useSplitClient(undefined, undefined, undefined, updateOptions);
146+
rendersCount++;
147+
return null;
148+
}
149+
150+
function Component(updateOptions) {
151+
return (
152+
<SplitFactory factory={outerFactory} >
153+
<InnerComponent {...updateOptions} />
154+
</SplitFactory>
155+
)
156+
}
157+
158+
const wrapper = render(<Component />);
159+
160+
act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render
161+
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default
162+
expect(rendersCount).toBe(2);
163+
164+
wrapper.rerender(<Component updateOnSdkUpdate={true} />); // trigger re-render
165+
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now
166+
167+
expect(rendersCount).toBe(4);
168+
169+
wrapper.rerender(<Component updateOnSdkUpdate={false} />); // trigger re-render
170+
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now
171+
172+
expect(rendersCount).toBe(5);
73173
});

src/__tests__/useSplitTreatments.test.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SplitTreatments } from '../SplitTreatments';
1616
import { SplitContext } from '../SplitContext';
1717
import { ISplitTreatmentsChildProps } from '../types';
1818

19-
function validateTreatments({ treatments, isReady, isReadyFromCache}: ISplitTreatmentsChildProps) {
19+
function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) {
2020
if (isReady || isReadyFromCache) {
2121
expect(treatments).toEqual({
2222
split_test: {
@@ -34,12 +34,12 @@ function validateTreatments({ treatments, isReady, isReadyFromCache}: ISplitTrea
3434
}
3535
}
3636

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

42-
let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0;
42+
let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0;
4343

4444
render(
4545
<SplitFactory factory={outerFactory} >
@@ -51,7 +51,7 @@ test('useSplitTreatments', () => {
5151
{() => { countSplitTreatments++; return null }}
5252
</SplitTreatments>
5353
{React.createElement(() => {
54-
const context = useSplitTreatments(['split_test'], { att1: 'att1' } );
54+
const context = useSplitTreatments(['split_test'], { att1: 'att1' });
5555
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
5656
validateTreatments(context);
5757
countUseSplitTreatments++;
@@ -64,6 +64,13 @@ test('useSplitTreatments', () => {
6464
countUseSplitTreatmentsUser2++;
6565
return null;
6666
})}
67+
{React.createElement(() => {
68+
const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true });
69+
expect(context.client).toBe(user2Client);
70+
validateTreatments(context);
71+
countUseSplitTreatmentsUser2WithUpdate++;
72+
return null;
73+
})}
6774
</>
6875
</SplitFactory>
6976
);
@@ -86,6 +93,8 @@ test('useSplitTreatments', () => {
8693

8794
// If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache.
8895
expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2);
89-
expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(2);
96+
// If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event.
97+
expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3);
98+
expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5);
9099
expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined);
91100
});

src/useSplitClient.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { SplitContext } from './SplitContext';
33
import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils';
4-
import { ISplitContextValues } from './types';
4+
import { ISplitContextValues, IUpdateProps } from './types';
55

66
export const DEFAULT_UPDATE_OPTIONS = {
77
updateOnSdkUpdate: false,
@@ -17,7 +17,11 @@ export const DEFAULT_UPDATE_OPTIONS = {
1717
* @return A Split Context object
1818
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
1919
*/
20-
export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues {
20+
export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues {
21+
const {
22+
updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate
23+
} = { ...DEFAULT_UPDATE_OPTIONS, ...options };
24+
2125
const context = React.useContext(SplitContext);
2226
const { client: contextClient, factory } = context;
2327

@@ -33,37 +37,23 @@ export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, att
3337
React.useEffect(() => {
3438
if (!client) return;
3539

36-
const setReady = () => {
37-
if (DEFAULT_UPDATE_OPTIONS.updateOnSdkReady) setLastUpdate(client.lastUpdate);
38-
}
39-
40-
const setReadyFromCache = () => {
41-
if (DEFAULT_UPDATE_OPTIONS.updateOnSdkReadyFromCache) setLastUpdate(client.lastUpdate);
42-
}
43-
44-
const setTimedout = () => {
45-
if (DEFAULT_UPDATE_OPTIONS.updateOnSdkTimedout) setLastUpdate(client.lastUpdate);
46-
}
47-
48-
const setUpdate = () => {
49-
if (DEFAULT_UPDATE_OPTIONS.updateOnSdkUpdate) setLastUpdate(client.lastUpdate);
50-
}
40+
const update = () => setLastUpdate(client.lastUpdate);
5141

5242
// Subscribe to SDK events
5343
const status = getStatus(client);
54-
if (!status.isReady) client.once(client.Event.SDK_READY, setReady);
55-
if (!status.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache);
56-
if (!status.hasTimedout && !status.isReady) client.once(client.Event.SDK_READY_TIMED_OUT, setTimedout);
57-
client.on(client.Event.SDK_UPDATE, setUpdate);
44+
if (!status.isReady && updateOnSdkReady) client.once(client.Event.SDK_READY, update);
45+
if (!status.isReadyFromCache && updateOnSdkReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, update);
46+
if (!status.hasTimedout && !status.isReady && updateOnSdkTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, update);
47+
if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update);
5848

5949
return () => {
6050
// Unsubscribe from events
61-
client.off(client.Event.SDK_READY, setReady);
62-
client.off(client.Event.SDK_READY_FROM_CACHE, setReadyFromCache);
63-
client.off(client.Event.SDK_READY_TIMED_OUT, setTimedout);
64-
client.off(client.Event.SDK_UPDATE, setUpdate);
51+
client.off(client.Event.SDK_READY, update);
52+
client.off(client.Event.SDK_READY_FROM_CACHE, update);
53+
client.off(client.Event.SDK_READY_TIMED_OUT, update);
54+
client.off(client.Event.SDK_UPDATE, update);
6555
}
66-
}, [client]);
56+
}, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]);
6757

6858
return {
6959
factory, client, ...getStatus(client)

src/useSplitTreatments.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getControlTreatmentsWithConfig } from './constants';
22
import { IClientWithContext } from './utils';
3-
import { ISplitTreatmentsChildProps } from './types';
3+
import { ISplitTreatmentsChildProps, IUpdateProps } from './types';
44
import { useSplitClient } from './useSplitClient';
55

66
/**
@@ -10,8 +10,8 @@ import { useSplitClient } from './useSplitClient';
1010
* @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist.
1111
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
1212
*/
13-
export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps {
14-
const context = useSplitClient(key);
13+
export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps {
14+
const context = useSplitClient(key, undefined, undefined, options);
1515
const client = context.client;
1616
const treatments = client && (client as IClientWithContext).__getStatus().isOperational ?
1717
client.getTreatmentsWithConfig(splitNames, attributes) :

types/useSplitClient.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ISplitContextValues } from './types';
1+
import { ISplitContextValues, IUpdateProps } from './types';
22
export declare const DEFAULT_UPDATE_OPTIONS: {
33
updateOnSdkUpdate: boolean;
44
updateOnSdkTimedout: boolean;
@@ -12,4 +12,4 @@ export declare const DEFAULT_UPDATE_OPTIONS: {
1212
* @return A Split Context object
1313
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
1414
*/
15-
export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues;
15+
export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues;

types/useSplitTreatments.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ISplitTreatmentsChildProps } from './types';
1+
import { ISplitTreatmentsChildProps, IUpdateProps } from './types';
22
/**
33
* 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments).
44
* It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method.
55
*
66
* @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist.
77
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
88
*/
9-
export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps;
9+
export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps;

0 commit comments

Comments
 (0)