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+
1718import { UserAttributes } from '@optimizely/optimizely-sdk' ;
18- import { getLogger } from '@optimizely/js-sdk-logging' ;
19+ import { getLogger , LoggerFacade } from '@optimizely/js-sdk-logging' ;
1920
2021import { setupAutoUpdateListeners } from './autoUpdate' ;
21- import { VariableValuesObject , OnReadyResult } from './client' ;
22+ import { ReactSDKClient , VariableValuesObject , OnReadyResult } from './client' ;
2223import { 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+
4468interface 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