Skip to content

Commit c167f2a

Browse files
authored
feat: useExperiment hook rerenders when client setForcedVariation is called (#97)
Summary: Add mechanism in client to subscribe to setForcedVariation calls. In useExperiment, subscribe and re-compute decision after setForcedVariation is called. Test plan: - New unit tests - Manual testing
1 parent c4063b1 commit c167f2a

File tree

6 files changed

+105
-16
lines changed

6 files changed

+105
-16
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ The following type definitions are used in the `ReactSDKClient` interface:
491491
- `isFeatureEnabled(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): boolean` Return the enabled status for the given feature and user
492492
- `getEnabledFeatures(overrideUserId?: string, overrideAttributes?: UserAttributes): Array<string>`: Return the keys of all features enabled for the given user
493493
- `track(eventKey: string, overrideUserId?: string | EventTags, overrideAttributes?: UserAttributes, eventTags?: EventTags): void` Track an event to the Optimizely results backend
494-
- `setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean` Set a forced variation for the given experiment, variation, and user
494+
- `setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean` Set a forced variation for the given experiment, variation, and user. **Note**: calling `setForcedVariation` on a given client will trigger a re-render of all `useExperiment` hooks and `OptimizelyExperiment` components that are using that client.
495495
- `getForcedVariation(experiment: string, overrideUserId?: string): string | null` Get the forced variation for the given experiment, variation, and user
496496

497497
## Rollout or experiment a feature user-by-user
@@ -586,60 +586,60 @@ First-party code subject to copyrights held by Optimizely, Inc. and its contribu
586586

587587
This repository includes the following third party open source code:
588588

589-
[**hoist-non-react-statics**](https://github.com/mridgway/hoist-non-react-statics)
589+
[**hoist-non-react-statics**](https://github.com/mridgway/hoist-non-react-statics)
590590
Copyright &copy; 2015 Yahoo!, Inc.
591591
License: [BSD](https://github.com/mridgway/hoist-non-react-statics/blob/master/LICENSE.md)
592592

593-
[**js-tokens**](https://github.com/lydell/js-tokens)
593+
[**js-tokens**](https://github.com/lydell/js-tokens)
594594
Copyright &copy; 2014, 2015, 2016, 2017, 2018, 2019 Simon Lydell
595595
License: [MIT](https://github.com/lydell/js-tokens/blob/master/LICENSE)
596596

597-
[**json-schema**](https://github.com/kriszyp/json-schema)
597+
[**json-schema**](https://github.com/kriszyp/json-schema)
598598
Copyright &copy; 2005-2015, The Dojo Foundation
599599
License: [BSD](https://github.com/kriszyp/json-schema/blob/master/LICENSE)
600600

601-
[**lodash**](https://github.com/lodash/lodash/)
601+
[**lodash**](https://github.com/lodash/lodash/)
602602
Copyright &copy; JS Foundation and other contributors
603603
License: [MIT](https://github.com/lodash/lodash/blob/master/LICENSE)
604604

605-
[**loose-envify**](https://github.com/zertosh/loose-envify)
605+
[**loose-envify**](https://github.com/zertosh/loose-envify)
606606
Copyright &copy; 2015 Andres Suarez <zertosh@gmail.com>
607607
License: [MIT](https://github.com/zertosh/loose-envify/blob/master/LICENSE)
608608

609-
[**node-murmurhash**](https://github.com/perezd/node-murmurhash)
609+
[**node-murmurhash**](https://github.com/perezd/node-murmurhash)
610610
Copyright &copy; 2012 Gary Court, Derek Perez
611611
License: [MIT](https://github.com/perezd/node-murmurhash/blob/master/README.md)
612612

613-
[**object-assign**](https://github.com/sindresorhus/object-assign)
613+
[**object-assign**](https://github.com/sindresorhus/object-assign)
614614
Copyright &copy; Sindre Sorhus (sindresorhus.com)
615615
License: [MIT](https://github.com/sindresorhus/object-assign/blob/master/license)
616616

617-
[**promise-polyfill**](https://github.com/taylorhakes/promise-polyfill)
617+
[**promise-polyfill**](https://github.com/taylorhakes/promise-polyfill)
618618
Copyright &copy; 2014 Taylor Hakes
619619
Copyright &copy; 2014 Forbes Lindesay
620620
License: [MIT](https://github.com/taylorhakes/promise-polyfill/blob/master/LICENSE)
621621

622-
[**prop-types**](https://github.com/facebook/prop-types)
622+
[**prop-types**](https://github.com/facebook/prop-types)
623623
Copyright &copy; 2013-present, Facebook, Inc.
624624
License: [MIT](https://github.com/facebook/prop-types/blob/master/LICENSE)
625625

626-
[**react-is**](https://github.com/facebook/react)
626+
[**react-is**](https://github.com/facebook/react)
627627
Copyright &copy; Facebook, Inc. and its affiliates.
628628
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)
629629

630-
[**react**](https://github.com/facebook/react)
630+
[**react**](https://github.com/facebook/react)
631631
Copyright &copy; Facebook, Inc. and its affiliates.
632632
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)
633633

634-
[**scheduler**](https://github.com/facebook/react)
634+
[**scheduler**](https://github.com/facebook/react)
635635
Copyright &copy; Facebook, Inc. and its affiliates.
636636
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)
637637

638-
[**utility-types**](https://github.com/piotrwitek/utility-types)
638+
[**utility-types**](https://github.com/piotrwitek/utility-types)
639639
Copyright &copy; 2016 Piotr Witek <piotrek.witek@gmail.com>
640640
License: [MIT](https://github.com/piotrwitek/utility-types/blob/master/LICENSE)
641641

642-
[**node-uuid**](https://github.com/kelektiv/node-uuid)
642+
[**node-uuid**](https://github.com/kelektiv/node-uuid)
643643
Copyright &copy; 2010-2016 Robert Kieffer and other contributors
644644
License: [MIT](https://github.com/kelektiv/node-uuid/blob/master/LICENSE.md)
645645

src/Experiment.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('<OptimizelyExperiment>', () => {
5252
attributes: {},
5353
},
5454
isReady: jest.fn().mockReturnValue(false),
55+
onForcedVariationsUpdate: jest.fn().mockReturnValue(() => {}),
5556
} as unknown) as ReactSDKClient;
5657
});
5758

src/client.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,4 +635,32 @@ describe('ReactSDKClient', () => {
635635
});
636636
});
637637
});
638+
639+
describe('onForcedVariationsUpdate', () => {
640+
let instance: ReactSDKClient;
641+
beforeEach(() => {
642+
instance = createInstance(config);
643+
instance.setUser({
644+
id: 'xxfueaojfe8&86',
645+
attributes: {
646+
foo: 'bar',
647+
},
648+
});
649+
});
650+
651+
it('calls the handler function when setForcedVariation is called', () => {
652+
const handler = jest.fn();
653+
instance.onForcedVariationsUpdate(handler);
654+
instance.setForcedVariation('my_exp', 'xxfueaojfe8&86', 'variation_a');
655+
expect(handler).toBeCalledTimes(1);
656+
});
657+
658+
it('removes the handler when the cleanup fn is called', () => {
659+
const handler = jest.fn();
660+
const cleanup = instance.onForcedVariationsUpdate(handler);
661+
cleanup();
662+
instance.setForcedVariation('my_exp', 'xxfueaojfe8&86', 'variation_a');
663+
expect(handler).not.toBeCalled();
664+
});
665+
});
638666
});

src/client.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type DisposeFn = () => void;
2828

2929
type OnUserUpdateHandler = (userInfo: UserContext) => void;
3030

31+
type OnForcedVariationsUpdateHandler = () => void;
32+
3133
export type OnReadyResult = {
3234
success: boolean;
3335
reason?: string;
@@ -131,6 +133,8 @@ export interface ReactSDKClient extends optimizely.Client {
131133
setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean;
132134

133135
getForcedVariation(experiment: string, overrideUserId?: string): string | null;
136+
137+
onForcedVariationsUpdate(handler: OnForcedVariationsUpdateHandler): DisposeFn;
134138
}
135139

136140
type UserContext = {
@@ -150,6 +154,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
150154
private userPromise: Promise<OnReadyResult>;
151155
private isUserPromiseResolved = false;
152156
private onUserUpdateHandlers: OnUserUpdateHandler[] = [];
157+
private onForcedVariationsUpdateHandlers: OnForcedVariationsUpdateHandler[] = [];
153158

154159
private readonly _client: optimizely.Client;
155160

@@ -237,6 +242,23 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
237242
};
238243
}
239244

245+
/**
246+
* Register a handler to be called whenever setForcedVariation is called on
247+
* this client. Returns a function that un-registers the handler when called.
248+
* @param {OnForcedVariationsUpdateHandler} handler
249+
* @returns {DisposeFn}
250+
*/
251+
onForcedVariationsUpdate(handler: OnForcedVariationsUpdateHandler): DisposeFn {
252+
this.onForcedVariationsUpdateHandlers.push(handler);
253+
254+
return (): void => {
255+
const ind = this.onForcedVariationsUpdateHandlers.indexOf(handler);
256+
if (ind > -1) {
257+
this.onForcedVariationsUpdateHandlers.splice(ind, 1);
258+
}
259+
};
260+
}
261+
240262
isReady(): boolean {
241263
return this.dataReadyPromiseFulfilled;
242264
}
@@ -594,7 +616,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
594616
if (finalUserId === null) {
595617
return false;
596618
}
597-
return this._client.setForcedVariation(experiment, finalUserId, finalVariationKey);
619+
const result = this._client.setForcedVariation(experiment, finalUserId, finalVariationKey);
620+
this.onForcedVariationsUpdateHandlers.forEach(handler => handler());
621+
return result;
598622
}
599623

600624
/**

src/hooks.spec.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('hooks', () => {
5151
let UseExperimentLoggingComponent: React.FunctionComponent<any>;
5252
let UseFeatureLoggingComponent: React.FunctionComponent<any>;
5353
let mockLog: jest.Mock;
54+
let forcedVariationUpdateCallbacks: Array<() => void>;
5455

5556
beforeEach(() => {
5657
getOnReadyPromise = ({ timeout = 0 }: any): Promise<OnReadyResult> =>
@@ -76,6 +77,7 @@ describe('hooks', () => {
7677
mockDelay = 10;
7778
readySuccess = true;
7879
notificationListenerCallbacks = [];
80+
forcedVariationUpdateCallbacks = [];
7981

8082
optimizelyMock = ({
8183
activate: activateMock,
@@ -97,6 +99,11 @@ describe('hooks', () => {
9799
attributes: {},
98100
},
99101
isReady: () => readySuccess,
102+
onForcedVariationsUpdate: jest.fn().mockImplementation(handler => {
103+
forcedVariationUpdateCallbacks.push(handler);
104+
return () => {};
105+
}),
106+
getForcedVariations: jest.fn().mockReturnValue({}),
100107
} as unknown) as ReactSDKClient;
101108

102109
mockLog = jest.fn();
@@ -359,6 +366,24 @@ describe('hooks', () => {
359366
component.update();
360367
expect(activateMock).not.toHaveBeenCalled();
361368
});
369+
370+
it('should re-render after setForcedVariation is called on the client', async () => {
371+
activateMock.mockReturnValue(null);
372+
const component = Enzyme.mount(
373+
<OptimizelyProvider optimizely={optimizelyMock}>
374+
<MyExperimentComponent options={{ autoUpdate: true }} />
375+
</OptimizelyProvider>
376+
);
377+
378+
component.update();
379+
expect(component.text()).toBe('null|true|false');
380+
381+
activateMock.mockReturnValue('12345');
382+
forcedVariationUpdateCallbacks[0]();
383+
384+
component.update();
385+
expect(component.text()).toBe('12345|true|false');
386+
});
362387
});
363388

364389
describe('useFeature', () => {

src/hooks.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,17 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
222222
return (): void => {};
223223
}, [isClientReady, options.autoUpdate, optimizely, experimentKey, getCurrentDecision]);
224224

225+
useEffect(
226+
() =>
227+
optimizely.onForcedVariationsUpdate(() => {
228+
setState(prevState => ({
229+
...prevState,
230+
...getCurrentDecision(),
231+
}));
232+
}),
233+
[getCurrentDecision, optimizely]
234+
);
235+
225236
return [state.variation, state.clientReady, state.didTimeout];
226237
};
227238

0 commit comments

Comments
 (0)