Skip to content

Commit e2cf17c

Browse files
author
James Fox
authored
chore (Hooks): Move common client ready logic to a separate function (#34)
## Summary In preparation to introduce a `useExperiment` hook, this PR moves the logic for waiting for the client to become ready, setting initial state, and configuring autoupdate to a shared function, which is passed to `useEffect` There are a lot of variables that need to be passed, but I think this is much preferable to duplicating the somewhat intricate & nuanced logic that exists there. ## Test Plan Existing unit tests cover this functionality, and more will be added with the introduction of `useExperiment`
1 parent 14a80f2 commit e2cf17c

File tree

2 files changed

+110
-66
lines changed

2 files changed

+110
-66
lines changed

src/autoUpdate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ReactSDKClient } from './client';
2121
interface AutoUpdate {
2222
(
2323
optimizely: ReactSDKClient,
24-
type: 'feature' | 'experiment',
24+
type: 'Feature' | 'Experiment',
2525
value: string,
2626
logger: LoggerFacade,
2727
callback: () => void
@@ -41,15 +41,15 @@ export const setupAutoUpdateListeners: AutoUpdate = (optimizely, type, value, lo
4141
callback();
4242
}
4343
);
44-
const unregisterConfigUpdateListener = () =>
44+
const unregisterConfigUpdateListener: () => void = () =>
4545
optimizely.notificationCenter.removeNotificationListener(optimizelyNotificationId);
4646

4747
const unregisterUserListener = optimizely.onUserUpdate(() => {
4848
logger.info(`User update, ${loggerSuffix}`);
4949
callback();
5050
});
5151

52-
return () => {
52+
return (): void => {
5353
unregisterConfigUpdateListener();
5454
unregisterUserListener();
5555
};

src/hooks.ts

Lines changed: 107 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,60 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { useCallback, useContext, useEffect, useState } from 'react';
16+
import { Dispatch, EffectCallback, SetStateAction, useCallback, useContext, useEffect, useState } from 'react';
17+
1718
import { UserAttributes } from '@optimizely/optimizely-sdk';
18-
import { getLogger } from '@optimizely/js-sdk-logging';
19+
import { getLogger, LoggerFacade } from '@optimizely/js-sdk-logging';
1920

2021
import { setupAutoUpdateListeners } from './autoUpdate';
21-
import { VariableValuesObject, OnReadyResult } from './client';
22+
import { ReactSDKClient, VariableValuesObject, OnReadyResult } from './client';
2223
import { OptimizelyContext } from './Context';
2324

24-
const useFeatureLogger = getLogger('useFeature');
25-
26-
type UseFeatureState = {
27-
isEnabled: boolean;
28-
variables: VariableValuesObject;
29-
};
30-
31-
type ClientReady = boolean;
32-
type DidTimeout = boolean;
25+
enum HookType {
26+
EXPERIMENT = 'Experiment',
27+
FEATURE = 'Feature',
28+
}
3329

34-
type UseFeatureOptions = {
30+
type HookOptions = {
3531
autoUpdate?: boolean;
3632
timeout?: number;
3733
};
3834

39-
type UseFeatureOverrides = {
35+
type HookOverrides = {
4036
overrideUserId?: string;
4137
overrideAttributes?: UserAttributes;
4238
};
4339

40+
type ClientReady = boolean;
41+
42+
type DidTimeout = boolean;
43+
44+
interface HookStateBase {
45+
clientReady: ClientReady;
46+
didTimeout: DidTimeout;
47+
}
48+
49+
// TODO - Get these from the core SDK once it's typed
50+
interface ExperimentDecisionValues {
51+
variation: string | null;
52+
}
53+
54+
// TODO - Get these from the core SDK once it's typed
55+
interface FeatureDecisionValues {
56+
isEnabled: boolean;
57+
variables: VariableValuesObject;
58+
}
59+
60+
interface UseExperimentState extends HookStateBase, ExperimentDecisionValues {}
61+
62+
interface UseFeatureState extends HookStateBase, FeatureDecisionValues {}
63+
64+
type HookState = UseExperimentState | UseFeatureState;
65+
66+
type CurrentDecisionValues = ExperimentDecisionValues | FeatureDecisionValues;
67+
4468
interface UseFeature {
45-
(featureKey: string, options?: UseFeatureOptions, overrides?: UseFeatureOverrides): [
69+
(featureKey: string, options?: HookOptions, overrides?: HookOverrides): [
4670
UseFeatureState['isEnabled'],
4771
UseFeatureState['variables'],
4872
ClientReady,
@@ -51,84 +75,104 @@ interface UseFeature {
5175
}
5276

5377
/**
54-
* A React Hook that retrieves the status of a feature flag and its variables, optionally
55-
* auto updating those values based on underlying user or datafile changes.
56-
*
57-
* Note: The react client can become ready AFTER the timeout period.
58-
* ClientReady and DidTimeout provide signals to handle this scenario.
78+
* A function which waits for the optimizely client instance passed to become
79+
* ready and then sets up initial state and (optionally) autoUpdate listeners
80+
* for the hook type specified.
5981
*/
60-
export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) => {
61-
const { isServerSide, optimizely, timeout } = useContext(OptimizelyContext);
62-
if (!optimizely) {
63-
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
64-
}
65-
const finalReadyTimeout: number | undefined = options.timeout !== undefined ? options.timeout : timeout;
66-
67-
// Helper function to return the current values for isEnabled and variables.
68-
const getCurrentValues = useCallback(
69-
() => ({
70-
isEnabled: optimizely.isFeatureEnabled(featureKey, overrides.overrideUserId, overrides.overrideAttributes),
71-
variables: optimizely.getFeatureVariables(featureKey, overrides.overrideUserId, overrides.overrideAttributes),
72-
}),
73-
[featureKey, overrides]
74-
);
75-
76-
// Set the initial state immediately serverSide
77-
const [data, setData] = useState<UseFeatureState>(() => {
78-
if (isServerSide) {
79-
return getCurrentValues();
80-
}
81-
return { isEnabled: false, variables: {} };
82-
});
83-
84-
const [clientReady, setClientReady] = useState(isServerSide ? true : false);
85-
const [didTimeout, setDidTimeout] = useState(false);
86-
87-
useEffect(() => {
82+
const initializeWhenClientReadyFn = (
83+
type: HookType,
84+
name: string,
85+
optimizely: ReactSDKClient,
86+
options: HookOptions,
87+
timeout: number | undefined,
88+
setState: Dispatch<SetStateAction<HookState>>,
89+
getCurrentDecisionValues: () => CurrentDecisionValues
90+
): EffectCallback => {
91+
return (): (() => void) => {
8892
const cleanupFns: Array<() => void> = [];
93+
const finalReadyTimeout: number | undefined = options.timeout !== undefined ? options.timeout : timeout;
94+
const logger: LoggerFacade = getLogger(`use${type}`);
8995

9096
optimizely
9197
.onReady({ timeout: finalReadyTimeout })
9298
.then((res: OnReadyResult) => {
9399
if (res.success) {
94100
// didTimeout=false
95-
useFeatureLogger.info(`feature="${featureKey}" successfully set for user="${optimizely.user.id}"`);
101+
logger.info(`${type}="${name}" successfully set for user="${optimizely.user.id}"`);
96102
return;
97103
}
98-
setDidTimeout(true);
99-
useFeatureLogger.info(
100-
`feature="${featureKey}" could not be set before timeout of ${finalReadyTimeout}ms, reason="${res.reason ||
101-
''}"`
102-
);
104+
setState((state: HookState) => ({ ...state, didTimeout: true }));
105+
logger.info(`${type}="${name}" could not be set before timeout of ${timeout}ms, reason="${res.reason || ''}"`);
103106
// Since we timed out, wait for the dataReadyPromise to resolve before setting up.
104107
return res.dataReadyPromise!.then(() => {
105-
useFeatureLogger.info(`feature="${featureKey}" is now set, but after timeout.`);
108+
logger.info(`${type}="${name}" is now set, but after timeout.`);
106109
});
107110
})
108111
.then(() => {
109-
setClientReady(true);
110-
setData(getCurrentValues());
112+
setState((state: HookState) => ({ ...state, ...getCurrentDecisionValues(), clientReady: true }));
111113
if (options.autoUpdate) {
112114
cleanupFns.push(
113-
setupAutoUpdateListeners(optimizely, 'feature', featureKey, useFeatureLogger, () => {
115+
setupAutoUpdateListeners(optimizely, type, name, logger, () => {
114116
if (cleanupFns.length) {
115-
setData(getCurrentValues());
117+
setState((state: HookState) => ({ ...state, ...getCurrentDecisionValues() }));
116118
}
117119
})
118120
);
119121
}
120122
})
121123
.catch(() => {
122124
/* The user promise or core client promise rejected. */
123-
useFeatureLogger.error(`Error initializing client. The core client or user promise(s) rejected.`);
125+
logger.error(`Error initializing client. The core client or user promise(s) rejected.`);
124126
});
125127

126-
return () => {
128+
return (): void => {
127129
while (cleanupFns.length) {
128130
cleanupFns.shift()!();
129131
}
130132
};
131-
}, [optimizely]);
133+
};
134+
};
135+
136+
/**
137+
* A React Hook that retrieves the status of a feature flag and its variables, optionally
138+
* auto updating those values based on underlying user or datafile changes.
139+
*
140+
* Note: The react client can become ready AFTER the timeout period.
141+
* ClientReady and DidTimeout provide signals to handle this scenario.
142+
*/
143+
export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) => {
144+
const { isServerSide, optimizely, timeout } = useContext(OptimizelyContext);
145+
if (!optimizely) {
146+
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
147+
}
148+
149+
// Helper function to return the current values for isEnabled and variables.
150+
const getCurrentValues = useCallback<() => FeatureDecisionValues>(
151+
() => ({
152+
isEnabled: optimizely.isFeatureEnabled(featureKey, overrides.overrideUserId, overrides.overrideAttributes),
153+
variables: optimizely.getFeatureVariables(featureKey, overrides.overrideUserId, overrides.overrideAttributes),
154+
}),
155+
[featureKey, overrides]
156+
);
157+
158+
// Set the initial state immediately serverSide
159+
const [state, setState] = useState<UseFeatureState>(() => {
160+
const initialState = {
161+
isEnabled: false,
162+
variables: {},
163+
clientReady: isServerSide ? true : false,
164+
didTimeout: false,
165+
};
166+
if (isServerSide) {
167+
return { ...initialState, ...getCurrentValues() };
168+
}
169+
return initialState;
170+
});
171+
172+
useEffect(
173+
initializeWhenClientReadyFn(HookType.FEATURE, featureKey, optimizely, options, timeout, setState, getCurrentValues),
174+
[optimizely]
175+
);
132176

133-
return [data.isEnabled, data.variables, clientReady, didTimeout];
177+
return [state.isEnabled, state.variables, state.clientReady, state.didTimeout];
134178
};

0 commit comments

Comments
 (0)