13
13
* See the License for the specific language governing permissions and
14
14
* limitations under the License.
15
15
*/
16
- import { useCallback , useContext , useEffect , useState } from 'react' ;
16
+ import { Dispatch , EffectCallback , SetStateAction , useCallback , useContext , useEffect , useState } from 'react' ;
17
+
17
18
import { UserAttributes } from '@optimizely/optimizely-sdk' ;
18
- import { getLogger } from '@optimizely/js-sdk-logging' ;
19
+ import { getLogger , LoggerFacade } from '@optimizely/js-sdk-logging' ;
19
20
20
21
import { setupAutoUpdateListeners } from './autoUpdate' ;
21
- import { VariableValuesObject , OnReadyResult } from './client' ;
22
+ import { ReactSDKClient , VariableValuesObject , OnReadyResult } from './client' ;
22
23
import { OptimizelyContext } from './Context' ;
23
24
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
+ }
33
29
34
- type UseFeatureOptions = {
30
+ type HookOptions = {
35
31
autoUpdate ?: boolean ;
36
32
timeout ?: number ;
37
33
} ;
38
34
39
- type UseFeatureOverrides = {
35
+ type HookOverrides = {
40
36
overrideUserId ?: string ;
41
37
overrideAttributes ?: UserAttributes ;
42
38
} ;
43
39
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
+
44
68
interface UseFeature {
45
- ( featureKey : string , options ?: UseFeatureOptions , overrides ?: UseFeatureOverrides ) : [
69
+ ( featureKey : string , options ?: HookOptions , overrides ?: HookOverrides ) : [
46
70
UseFeatureState [ 'isEnabled' ] ,
47
71
UseFeatureState [ 'variables' ] ,
48
72
ClientReady ,
@@ -51,84 +75,104 @@ interface UseFeature {
51
75
}
52
76
53
77
/**
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.
59
81
*/
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 ) => {
88
92
const cleanupFns : Array < ( ) => void > = [ ] ;
93
+ const finalReadyTimeout : number | undefined = options . timeout !== undefined ? options . timeout : timeout ;
94
+ const logger : LoggerFacade = getLogger ( `use${ type } ` ) ;
89
95
90
96
optimizely
91
97
. onReady ( { timeout : finalReadyTimeout } )
92
98
. then ( ( res : OnReadyResult ) => {
93
99
if ( res . success ) {
94
100
// 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 } "` ) ;
96
102
return ;
97
103
}
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 || '' } "` ) ;
103
106
// Since we timed out, wait for the dataReadyPromise to resolve before setting up.
104
107
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.` ) ;
106
109
} ) ;
107
110
} )
108
111
. then ( ( ) => {
109
- setClientReady ( true ) ;
110
- setData ( getCurrentValues ( ) ) ;
112
+ setState ( ( state : HookState ) => ( { ...state , ...getCurrentDecisionValues ( ) , clientReady : true } ) ) ;
111
113
if ( options . autoUpdate ) {
112
114
cleanupFns . push (
113
- setupAutoUpdateListeners ( optimizely , 'feature' , featureKey , useFeatureLogger , ( ) => {
115
+ setupAutoUpdateListeners ( optimizely , type , name , logger , ( ) => {
114
116
if ( cleanupFns . length ) {
115
- setData ( getCurrentValues ( ) ) ;
117
+ setState ( ( state : HookState ) => ( { ... state , ... getCurrentDecisionValues ( ) } ) ) ;
116
118
}
117
119
} )
118
120
) ;
119
121
}
120
122
} )
121
123
. catch ( ( ) => {
122
124
/* 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.` ) ;
124
126
} ) ;
125
127
126
- return ( ) => {
128
+ return ( ) : void => {
127
129
while ( cleanupFns . length ) {
128
130
cleanupFns . shift ( ) ! ( ) ;
129
131
}
130
132
} ;
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
+ ) ;
132
176
133
- return [ data . isEnabled , data . variables , clientReady , didTimeout ] ;
177
+ return [ state . isEnabled , state . variables , state . clientReady , state . didTimeout ] ;
134
178
} ;
0 commit comments