Skip to content

Commit 2bedde4

Browse files
[FSSDK-10773] hook logic improvement
1 parent 2d10863 commit 2bedde4

4 files changed

Lines changed: 69 additions & 9 deletions

File tree

src/hooks.spec.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, 2023, 2024 Optimizely
2+
* Copyright 2022, 2023, 2024, 2026 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -1080,6 +1080,55 @@ describe('hooks', () => {
10801080
});
10811081
});
10821082

1083+
it('should make sync decision when qualifiedSegments are provided via OptimizelyProvider with ODP integrated', async () => {
1084+
const segments = ['segment1', 'segment2'];
1085+
1086+
// Mock ODP integration
1087+
(optimizelyMock as any)._client = {
1088+
isOdpIntegrated: () => true,
1089+
};
1090+
1091+
// Client not fully ready yet (fetchQualifiedSegments still pending)
1092+
(optimizelyMock.isReady as any) = () => false;
1093+
(optimizelyMock.getIsReadyPromiseFulfilled as any) = () => false;
1094+
1095+
// Simulate setUser synchronously making config and userContext (with segments) available
1096+
// This mirrors real behavior: setUser synchronously sets userContext.qualifiedSegments
1097+
// before awaiting onReady
1098+
(optimizelyMock.setUser as jest.Mock).mockImplementation((_user, qualifiedSegments) => {
1099+
(optimizelyMock.getOptimizelyConfig as jest.Mock).mockReturnValue({});
1100+
(optimizelyMock.getUserContext as jest.Mock).mockReturnValue({ qualifiedSegments });
1101+
});
1102+
1103+
decideMock.mockReturnValue({ ...defaultDecision, enabled: true });
1104+
1105+
// Hold onReady so client never becomes "ready" during initial render
1106+
let resolveReadyPromise: (result: { success: boolean }) => void;
1107+
const readyPromise = new Promise<any>((res) => {
1108+
resolveReadyPromise = res;
1109+
});
1110+
getOnReadyPromise = (): Promise<any> => readyPromise;
1111+
1112+
render(
1113+
<OptimizelyProvider
1114+
optimizely={optimizelyMock}
1115+
user={{ id: 'testuser', attributes: {} }}
1116+
qualifiedSegments={segments}
1117+
>
1118+
<UseDecisionLoggingComponent />
1119+
</OptimizelyProvider>
1120+
);
1121+
1122+
// hasConfigAndUserInfo returns true (config + userContext + odpIntegrated + qualifiedSegments)
1123+
// so useDecision should evaluate a sync decision even though client is not ready
1124+
expect(mockLog).toHaveBeenCalledTimes(1);
1125+
expect(mockLog).toHaveBeenCalledWith(true);
1126+
// Cleanup: resolve onReady promise to prevent test timeout
1127+
await act(async () => {
1128+
resolveReadyPromise!({ success: true });
1129+
});
1130+
});
1131+
10831132
it('should re-render after updating the override user ID argument', async () => {
10841133
decideMock.mockReturnValue({ ...defaultDecision });
10851134
const { rerender } = render(

src/hooks.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,15 @@ function useCompareAttrsMemoize(value: UserAttributes | undefined): UserAttribut
249249
return ref.current;
250250
}
251251

252+
function hasConfigAndUserInfo(optimizely: ReactSDKClient | null): boolean {
253+
const hasConfig = !!optimizely?.getOptimizelyConfig();
254+
const userContext = optimizely?.getUserContext();
255+
// @ts-ignore
256+
const isOdpIntegrated = optimizely?._client?.isOdpIntegrated();
257+
const areSegmentsAvailable = !!(isOdpIntegrated && userContext?.qualifiedSegments !== undefined);
258+
return !!(hasConfig && userContext && (!isOdpIntegrated || areSegmentsAvailable));
259+
}
260+
252261
/**
253262
* A React Hook that retrieves the variation for an experiment, optionally
254263
* auto updating that value based on underlying user or datafile changes.
@@ -270,9 +279,10 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
270279

271280
const isClientReady = isServerSide || !!optimizely?.isReady();
272281
const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled();
273-
const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext());
282+
const isExperimentReady = hasConfigAndUserInfo(optimizely) || isClientReady;
283+
274284
const [state, setState] = useState<ExperimentDecisionValues & InitializationState>(() => {
275-
const decisionState = canMakeDecision ? getCurrentDecision() : { variation: null };
285+
const decisionState = isExperimentReady ? getCurrentDecision() : { variation: null };
276286
return {
277287
...decisionState,
278288
clientReady: isClientReady,
@@ -368,10 +378,10 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})
368378

369379
const isClientReady = isServerSide || !!optimizely?.isReady();
370380
const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled();
371-
const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext());
381+
const isFeatureReady = hasConfigAndUserInfo(optimizely) || isClientReady;
372382

373383
const [state, setState] = useState<FeatureDecisionValues & InitializationState>(() => {
374-
const decisionState = canMakeDecision ? getCurrentDecision() : { isEnabled: false, variables: {} };
384+
const decisionState = isFeatureReady ? getCurrentDecision() : { isEnabled: false, variables: {} };
375385
return {
376386
...decisionState,
377387
clientReady: isClientReady,
@@ -468,10 +478,11 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {})
468478

469479
const isClientReady = isServerSide || !!optimizely?.isReady();
470480
const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled();
471-
const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext());
481+
482+
const isDecisionReady = hasConfigAndUserInfo(optimizely) || isClientReady;
472483

473484
const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => {
474-
const decisionState = canMakeDecision
485+
const decisionState = isDecisionReady
475486
? getCurrentDecision()
476487
: {
477488
decision: defaultDecision,

src/reactUtils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2026 Optimizely
2+
* Copyright 2019, 2026 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2026 Optimizely
2+
* Copyright 2019, 2026 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)