Skip to content

Commit

Permalink
avoid throwing in react hooks if the optimizely client is not availab…
Browse files Browse the repository at this point in the history
…le from the context

this allows an application to catch a thrown error from `createInstance` and use `null` as a fallback for the OptimizelyProvider, keeping the application working instead of crashing.

see optimizely#134 for more info

fixes optimizely#134
  • Loading branch information
fabb committed Nov 15, 2021
1 parent d370252 commit 9c0d3dc
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 30 deletions.
11 changes: 10 additions & 1 deletion src/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { areUsersEqual, UserInfo } from './utils';
const logger = getLogger('<OptimizelyProvider>');

interface OptimizelyProviderProps {
optimizely: ReactSDKClient;
optimizely: ReactSDKClient | null;
timeout?: number;
isServerSide?: boolean;
user?: Promise<UserInfo> | UserInfo;
Expand All @@ -42,6 +42,10 @@ export class OptimizelyProvider extends React.Component<OptimizelyProviderProps,
super(props);
const { optimizely, userId, userAttributes, user } = props;

if (!optimizely) {
return;
}

// check if user id/attributes are provided as props and set them ReactSDKClient
let finalUser: UserInfo | null = null;

Expand Down Expand Up @@ -76,6 +80,11 @@ export class OptimizelyProvider extends React.Component<OptimizelyProviderProps,
return;
}
const { optimizely } = this.props;

if (!optimizely) {
return;
}

if (this.props.user && 'id' in this.props.user) {
if (!optimizely.user.id) {
// no user is set in optimizely, update
Expand Down
65 changes: 36 additions & 29 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,16 @@ function useCompareAttrsMemoize(value: UserAttributes | undefined): UserAttribut
*/
export const useExperiment: UseExperiment = (experimentKey, options = {}, overrides = {}) => {
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
if (!optimizely) {
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
}

const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
const getCurrentDecision: () => ExperimentDecisionValues = useCallback(
() => ({
variation: optimizely.activate(experimentKey, overrides.overrideUserId, overrideAttrs),
variation: optimizely ? optimizely.activate(experimentKey, overrides.overrideUserId, overrideAttrs) : null,
}),
[optimizely, experimentKey, overrides.overrideUserId, overrideAttrs]
);

const isClientReady = isServerSide || optimizely.isReady();
const isClientReady = isServerSide || (optimizely ? optimizely.isReady() : false);
const [state, setState] = useState<ExperimentDecisionValues & InitializationState>(() => {
const decisionState = isClientReady ? getCurrentDecision() : { variation: null };
return {
Expand Down Expand Up @@ -210,7 +207,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri

const finalReadyTimeout = options.timeout !== undefined ? options.timeout : timeout;
useEffect(() => {
if (!isClientReady) {
if (optimizely && !isClientReady) {
subscribeToInitialization(optimizely, finalReadyTimeout, initState => {
setState({
...getCurrentDecision(),
Expand All @@ -221,7 +218,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
}, [isClientReady, finalReadyTimeout, getCurrentDecision, optimizely]);

useEffect(() => {
if (options.autoUpdate) {
if (optimizely && options.autoUpdate) {
return setupAutoUpdateListeners(optimizely, HookType.EXPERIMENT, experimentKey, hooksLogger, () => {
setState(prevState => ({
...prevState,
Expand All @@ -232,16 +229,16 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
return (): void => {};
}, [isClientReady, options.autoUpdate, optimizely, experimentKey, getCurrentDecision]);

useEffect(
() =>
useEffect(() => {
if (optimizely) {
optimizely.onForcedVariationsUpdate(() => {
setState(prevState => ({
...prevState,
...getCurrentDecision(),
}));
}),
[getCurrentDecision, optimizely]
);
});
}
}, [getCurrentDecision, optimizely]);

return [state.variation, state.clientReady, state.didTimeout];
};
Expand All @@ -255,20 +252,17 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
*/
export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) => {
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
if (!optimizely) {
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
}

const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
const getCurrentDecision: () => FeatureDecisionValues = useCallback(
() => ({
isEnabled: optimizely.isFeatureEnabled(featureKey, overrides.overrideUserId, overrideAttrs),
variables: optimizely.getFeatureVariables(featureKey, overrides.overrideUserId, overrideAttrs),
isEnabled: optimizely ? optimizely.isFeatureEnabled(featureKey, overrides.overrideUserId, overrideAttrs) : false,
variables: optimizely ? optimizely.getFeatureVariables(featureKey, overrides.overrideUserId, overrideAttrs) : {},
}),
[optimizely, featureKey, overrides.overrideUserId, overrideAttrs]
);

const isClientReady = isServerSide || optimizely.isReady();
const isClientReady = isServerSide || (optimizely ? optimizely.isReady() : false);
const [state, setState] = useState<FeatureDecisionValues & InitializationState>(() => {
const decisionState = isClientReady ? getCurrentDecision() : { isEnabled: false, variables: {} };
return {
Expand Down Expand Up @@ -297,7 +291,7 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})

const finalReadyTimeout = options.timeout !== undefined ? options.timeout : timeout;
useEffect(() => {
if (!isClientReady) {
if (optimizely && !isClientReady) {
subscribeToInitialization(optimizely, finalReadyTimeout, initState => {
setState({
...getCurrentDecision(),
Expand All @@ -308,7 +302,7 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})
}, [isClientReady, finalReadyTimeout, getCurrentDecision, optimizely]);

useEffect(() => {
if (options.autoUpdate) {
if (optimizely && options.autoUpdate) {
return setupAutoUpdateListeners(optimizely, HookType.FEATURE, featureKey, hooksLogger, () => {
setState(prevState => ({
...prevState,
Expand All @@ -331,22 +325,35 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})
*/
export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) => {
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
if (!optimizely) {
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
}

const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
const getCurrentDecision: () => { decision: OptimizelyDecision } = useCallback(
() => ({
decision: optimizely.decide(flagKey, options.decideOptions, overrides.overrideUserId, overrideAttrs)
decision: optimizely
? optimizely.decide(flagKey, options.decideOptions, overrides.overrideUserId, overrideAttrs)
: {
enabled: false,
variationKey: null,
userContext: { id: null },
variables: {},
ruleKey: null,
flagKey,
reasons: [],
},
}),
[optimizely, flagKey, overrides.overrideUserId, overrideAttrs, options.decideOptions]
);

const isClientReady = isServerSide || optimizely.isReady();
const isClientReady = isServerSide || (optimizely ? optimizely.isReady() : false);
const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => {
const decisionState = isClientReady? getCurrentDecision()
: { decision: createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', { id: overrides.overrideUserId || null, attributes: overrideAttrs}) };
const decisionState = isClientReady
? getCurrentDecision()
: {
decision: createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', {
id: overrides.overrideUserId || null,
attributes: overrideAttrs,
}),
};
return {
...decisionState,
clientReady: isClientReady,
Expand All @@ -373,7 +380,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {})

const finalReadyTimeout = options.timeout !== undefined ? options.timeout : timeout;
useEffect(() => {
if (!isClientReady) {
if (optimizely && !isClientReady) {
subscribeToInitialization(optimizely, finalReadyTimeout, initState => {
setState({
...getCurrentDecision(),
Expand All @@ -384,7 +391,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {})
}, [isClientReady, finalReadyTimeout, getCurrentDecision, optimizely]);

useEffect(() => {
if (options.autoUpdate) {
if (optimizely && options.autoUpdate) {
return setupAutoUpdateListeners(optimizely, HookType.FEATURE, flagKey, hooksLogger, () => {
setState(prevState => ({
...prevState,
Expand Down

0 comments on commit 9c0d3dc

Please sign in to comment.