|
14 | 14 | * limitations under the License.
|
15 | 15 | */
|
16 | 16 | import * as React from 'react';
|
17 |
| -import { withOptimizely, WithOptimizelyProps } from './withOptimizely'; |
18 |
| -import { VariationProps } from './Variation'; |
19 |
| -import { VariableValuesObject, OnReadyResult, DEFAULT_ON_READY_TIMEOUT } from './client'; |
20 |
| -import * as logging from '@optimizely/js-sdk-logging'; |
21 | 17 |
|
22 |
| -const logger = logging.getLogger('<OptimizelyExperiment>'); |
| 18 | +import { UserAttributes } from '@optimizely/optimizely-sdk'; |
23 | 19 |
|
24 |
| -export type ChildrenRenderFunction = (variableValues: VariableValuesObject) => React.ReactNode; |
| 20 | +import { useExperiment } from './hooks'; |
| 21 | +import { VariationProps } from './Variation'; |
| 22 | +import { withOptimizely, WithOptimizelyProps } from './withOptimizely'; |
25 | 23 |
|
26 |
| -type ChildRenderFunction = (variation: string | null) => React.ReactNode; |
| 24 | +export type ChildrenRenderFunction = ( |
| 25 | + variation: string | null, |
| 26 | + clientReady?: boolean, |
| 27 | + didTimeout?: boolean |
| 28 | +) => React.ReactNode; |
27 | 29 |
|
28 | 30 | export interface ExperimentProps extends WithOptimizelyProps {
|
29 | 31 | // TODO add support for overrideUserId
|
30 | 32 | experiment: string;
|
31 | 33 | autoUpdate?: boolean;
|
32 | 34 | timeout?: number;
|
33 |
| - children: React.ReactNode | ChildRenderFunction; |
34 |
| -} |
35 |
| - |
36 |
| -export interface ExperimentState { |
37 |
| - canRender: boolean; |
38 |
| - variation: string | null; |
| 35 | + overrideUserId?: string; |
| 36 | + overrideAttributes?: UserAttributes; |
| 37 | + children: React.ReactNode | ChildrenRenderFunction; |
39 | 38 | }
|
40 | 39 |
|
41 |
| -export class Experiment extends React.Component<ExperimentProps, ExperimentState> { |
42 |
| - private optimizelyNotificationId?: number; |
43 |
| - private unregisterUserListener: () => void = () => {}; |
44 |
| - private autoUpdate = false; |
45 |
| - |
46 |
| - constructor(props: ExperimentProps) { |
47 |
| - super(props); |
48 |
| - |
49 |
| - const { autoUpdate, isServerSide, optimizely, experiment } = props; |
50 |
| - this.autoUpdate = !!autoUpdate; |
51 |
| - |
52 |
| - if (isServerSide) { |
53 |
| - if (!optimizely) { |
54 |
| - throw new Error('optimizely prop must be supplied'); |
55 |
| - } |
56 |
| - const variation = optimizely.activate(experiment); |
57 |
| - this.state = { |
58 |
| - canRender: true, |
59 |
| - variation, |
60 |
| - }; |
61 |
| - } else { |
62 |
| - this.state = { |
63 |
| - canRender: false, |
64 |
| - variation: null, |
65 |
| - }; |
66 |
| - } |
| 40 | +const Experiment: React.FunctionComponent<ExperimentProps> = props => { |
| 41 | + const { experiment, autoUpdate, timeout, overrideUserId, overrideAttributes, children } = props; |
| 42 | + const [variation, clientReady, didTimeout] = useExperiment( |
| 43 | + experiment, |
| 44 | + { timeout, autoUpdate }, |
| 45 | + { overrideUserId, overrideAttributes } |
| 46 | + ); |
| 47 | + |
| 48 | + if (!clientReady && !didTimeout) { |
| 49 | + // Only block rendering while were waiting for the client within the allowed timeout. |
| 50 | + return null; |
67 | 51 | }
|
68 | 52 |
|
69 |
| - componentDidMount() { |
70 |
| - const { experiment, optimizely, optimizelyReadyTimeout, isServerSide, timeout } = this.props; |
71 |
| - if (!optimizely) { |
72 |
| - throw new Error('optimizely prop must be supplied'); |
73 |
| - } |
74 |
| - if (isServerSide) { |
75 |
| - return; |
76 |
| - } |
77 |
| - |
78 |
| - // allow overriding of the ready timeout via the `timeout` prop passed to <Experiment /> |
79 |
| - const finalReadyTimeout: number | undefined = timeout !== undefined ? timeout : optimizelyReadyTimeout; |
80 |
| - |
81 |
| - optimizely.onReady({ timeout: finalReadyTimeout }).then((res: OnReadyResult) => { |
82 |
| - if (res.success) { |
83 |
| - logger.info('experiment="%s" successfully rendered for user="%s"', experiment, optimizely.user.id); |
84 |
| - } else { |
85 |
| - logger.info( |
86 |
| - 'experiment="%s" could not be checked before timeout of %sms, reason="%s" ', |
87 |
| - experiment, |
88 |
| - timeout === undefined ? DEFAULT_ON_READY_TIMEOUT : timeout, |
89 |
| - res.reason || '' |
90 |
| - ); |
91 |
| - } |
92 |
| - |
93 |
| - const variation = optimizely.activate(experiment); |
94 |
| - this.setState({ |
95 |
| - canRender: true, |
96 |
| - variation, |
97 |
| - }); |
98 |
| - if (this.autoUpdate) { |
99 |
| - this.setupAutoUpdateListeners(); |
100 |
| - } |
101 |
| - }); |
| 53 | + if (children != null && typeof children === 'function') { |
| 54 | + // Wrap the return value here in a Fragment to please the HOC's expected React.ComponentType |
| 55 | + // See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 |
| 56 | + return <>{(children as ChildrenRenderFunction)(variation, clientReady, didTimeout)}</>; |
102 | 57 | }
|
103 | 58 |
|
104 |
| - setupAutoUpdateListeners() { |
105 |
| - const { optimizely, experiment } = this.props; |
106 |
| - if (optimizely === null) { |
107 |
| - return; |
108 |
| - } |
109 |
| - |
110 |
| - this.optimizelyNotificationId = optimizely.notificationCenter.addNotificationListener( |
111 |
| - 'OPTIMIZELY_CONFIG_UPDATE', |
112 |
| - () => { |
113 |
| - logger.info( |
114 |
| - 'OPTIMIZELY_CONFIG_UPDATE, re-evaluating experiment="%s" for user="%s"', |
115 |
| - experiment, |
116 |
| - optimizely.user.id |
117 |
| - ); |
118 |
| - const variation = optimizely.activate(experiment); |
119 |
| - this.setState({ |
120 |
| - variation, |
121 |
| - }); |
122 |
| - } |
123 |
| - ); |
124 |
| - |
125 |
| - this.unregisterUserListener = optimizely.onUserUpdate(() => { |
126 |
| - logger.info('User update, re-evaluating experiment="%s" for user="%s"', experiment, optimizely.user.id); |
127 |
| - const variation = optimizely.activate(experiment); |
128 |
| - this.setState({ |
129 |
| - variation, |
130 |
| - }); |
131 |
| - }); |
132 |
| - } |
| 59 | + let match: React.ReactElement<VariationProps> | null = null; |
133 | 60 |
|
134 |
| - componentWillUnmount() { |
135 |
| - const { optimizely, isServerSide } = this.props; |
136 |
| - if (isServerSide || !this.autoUpdate) { |
| 61 | + // We use React.Children.forEach instead of React.Children.toArray().find() |
| 62 | + // here because toArray adds keys to all child elements and we do not want |
| 63 | + // to trigger an unmount/remount |
| 64 | + React.Children.forEach(children, (child: React.ReactElement<VariationProps>) => { |
| 65 | + if (match || !React.isValidElement(child)) { |
137 | 66 | return;
|
138 | 67 | }
|
139 |
| - if (optimizely && this.optimizelyNotificationId) { |
140 |
| - optimizely.notificationCenter.removeNotificationListener(this.optimizelyNotificationId); |
141 |
| - } |
142 |
| - this.unregisterUserListener(); |
143 |
| - } |
144 | 68 |
|
145 |
| - render() { |
146 |
| - const { children } = this.props; |
147 |
| - const { variation, canRender } = this.state; |
148 |
| - |
149 |
| - if (!canRender) { |
150 |
| - return null; |
151 |
| - } |
152 |
| - |
153 |
| - if (children != null && typeof children === 'function') { |
154 |
| - return (children as ChildRenderFunction)(variation); |
155 |
| - } |
156 |
| - |
157 |
| - let match: React.ReactElement<VariationProps> | null = null; |
158 |
| - |
159 |
| - // We use React.Children.forEach instead of React.Children.toArray().find() |
160 |
| - // here because toArray adds keys to all child elements and we do not want |
161 |
| - // to trigger an unmount/remount |
162 |
| - React.Children.forEach(this.props.children, (child: React.ReactElement<VariationProps>) => { |
163 |
| - if (match || !React.isValidElement(child)) { |
164 |
| - return; |
165 |
| - } |
166 |
| - |
167 |
| - if (child.props.variation) { |
168 |
| - if (variation === child.props.variation) { |
169 |
| - match = child; |
170 |
| - } |
171 |
| - } else if (child.props.default) { |
| 69 | + if (child.props.variation) { |
| 70 | + if (variation === child.props.variation) { |
172 | 71 | match = child;
|
173 | 72 | }
|
174 |
| - }); |
| 73 | + } else if (child.props.default) { |
| 74 | + match = child; |
| 75 | + } |
| 76 | + }); |
175 | 77 |
|
176 |
| - return match ? React.cloneElement(match, { variation: variation }) : null; |
177 |
| - } |
178 |
| -} |
| 78 | + return match ? React.cloneElement(match, { variation }) : null; |
| 79 | +}; |
179 | 80 |
|
180 | 81 | export const OptimizelyExperiment = withOptimizely(Experiment);
|
0 commit comments