Skip to content

Commit 5652dc9

Browse files
author
James Fox
authored
feat (Hooks): Add useFeature hook (#28)
## Summary This PR adds a React Hook called `useFeature`, enabling feature usage (with variables) via the hooks interface. The API designed here is intended to be feature complete with the `<OptimizelyFeature>` component. ### Justification - Why add hooks? Despite just being the new hotness in the React world, there are a couple good reasons to supporting the hooks paradigm in this SDK: - Easy use of features/variables outside the context of JSX markup - Allow consumers to avoid using class components (mostly in the interest of bundle size) - This could also be done by rewriting `<OptimizelyFeature>` as a functional component that itself leverages built in React hooks for state management #### TODO - [x] Update hook to return a third argument, `clientReady`, which will indicate whether or not the client is ready. - [x] Add more tests for usage of the options and overrides ### Example Usage _Directly from the README update:_ *arguments* * `feature : string` Key of the feature * `options : Object` * `autoUpdate : boolean` (optional) If true, this component will re-render in response to datafile or user changes. Default: `false`. * `timeout : number` (optional) Rendering timeout as described in the `OptimizelyProvider` section. Overrides any timeout set on the ancestor `OptimizelyProvider`. * `overrides : Object` * `overrideUserId : string` (optional) Override the userId for calls to `isFeatureEnabled` for this hook. * `overrideAttributes : optimizely.UserAttributes` (optional) Override the user attributes for calls to `isFeatureEnabled` for this hook. ```jsx import { useEffect } from 'react'; import { useFeature } from '@optimizely/react-sdk'; function LoginComponent() { const [isEnabled, variables] = useFeature('feature1', { autoUpdate: true }, { /* (Optional) User overrides */ }); useEffect(() => { document.title = isEnabled ? 'login1' : 'login2'; }, [isEnabled]); return ( <p> <a href={isEnabled ? "/login" : "/login2"}> {variables.loginText} </a> </p> ) } ``` ### AutoUpdate A utility was added to help manage auto updates of the feature and it's variables in `autoUpdate.ts`. Eventually, this utility can be used in a `useExperiment` hook. Additionally, the `useFeature` and `useExperiment` hooks could then handle all the state management and auto update logic for `<OptimizelyFeature>` and `<OptimizelyExperiment>`, allowing those to just be thin wrapper components. ## Test Plan Minimal unit tests have been added to test the base case(s) here. Additional testing of the optional parameters will be added once this overall approach is agreed upon. _Note: Until we upgrade to React 16.9, there will be errors in the `hooks.spec.tsx` unit tests about not wrapping state setting test actions in `act()`. React 16.9 introduces async support for `act()`, which will allow us to wrap our invocation of `await client.onReady()`_ ## Issues Addresses #6
1 parent 2fffb5c commit 5652dc9

File tree

11 files changed

+494
-18
lines changed

11 files changed

+494
-18
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### New Features
11+
12+
- Added `useFeature` hook
13+
- Can be used to retrieve the status of a feature flag and its variables. See [#28](https://github.com/optimizely/react-sdk/pull/28) for more details.
14+
15+
### Enhancements
16+
17+
- Exposed the entire context object used by
18+
- Enables support for using APIs which require passing reference to a context object, like `useContext`. [#27](https://github.com/optimizely/react-sdk/pull/27) for more details.
19+
20+
1021
## [1.2.0-alpha.1] - March 5th, 2020
1122

1223
### New Features

README.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ The `ReactSDKClient` client created via `createInstance` is the programmatic API
9898
- [JavaScript: Update datafiles](https://docs.developers.optimizely.com/full-stack/docs/javascript-update-datafiles)
9999

100100
*returns*
101-
- A `ReactSDKClient` instance.
101+
* A `ReactSDKClient` instance.
102102

103103
```jsx
104104
import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'
@@ -301,6 +301,53 @@ function FeatureComponent() {
301301
}
302302
```
303303

304+
305+
## `useFeature` Hook
306+
307+
A [React Hook](https://reactjs.org/docs/hooks-intro.html) to retrieve the status of a feature flag and its variables. This can be useful as an alternative to the `<OptimizelyFeature>` component or to use features & variables inside code that is not explicitly rendered.
308+
309+
*arguments*
310+
* `feature : string` Key of the feature
311+
* `options : Object`
312+
* `autoUpdate : boolean` (optional) If true, this hook will update the feature and it's variables in response to datafile or user changes. Default: `false`.
313+
* `timeout : number` (optional) Client timeout as described in the `OptimizelyProvider` section. Overrides any timeout set on the ancestor `OptimizelyProvider`.
314+
* `overrides : Object`
315+
* `overrideUserId : string` (optional) Override the userId for calls to `isFeatureEnabled` for this hook.
316+
* `overrideAttributes : optimizely.UserAttributes` (optional) Override the user attributes for calls to `isFeatureEnabled` for this hook.
317+
318+
*returns*
319+
320+
* `Array` of:
321+
* `isFeatureEnabled : boolean` - The `isFeatureEnabled` value for the `feature` provided.
322+
* `variables : VariableValuesObject` - The variable values for the `feature` provided
323+
* `clientReady : boolean` - Whether or not the underlying `ReactSDKClient` instance is ready or not.
324+
* `didTimeout : boolean` - Whether or not the underlying `ReactSDKClient` became ready within the allowed `timeout` range.
325+
326+
_Note: `clientReady` can be true even if `didTimeout` is also true. This indicates that the client became ready *after* the timeout period._
327+
328+
### Render something if feature is enabled
329+
330+
```jsx
331+
import { useEffect } from 'react';
332+
import { useFeature } from '@optimizely/react-sdk';
333+
334+
function LoginComponent() {
335+
const [isEnabled, variables] = useFeature('feature1', { autoUpdate: true }, { /* (Optional) User overrides */ });
336+
useEffect(() => {
337+
document.title = isEnabled ? 'login1' : 'login2';
338+
}, [isEnabled]);
339+
340+
return (
341+
<p>
342+
<a href={isEnabled ? "/login" : "/login2"}>
343+
{variables.loginText}
344+
</a>
345+
</p>
346+
)
347+
}
348+
```
349+
350+
304351
## `withOptimizely`
305352

306353
Any component under the `<OptimizelyProvider>` can access the Optimizely `ReactSDKClient` via the higher-order component (HoC) `withOptimizely`.
@@ -377,7 +424,7 @@ The following type definitions are used in the `ReactSDKClient` interface:
377424

378425
`ReactSDKClient` instances have the methods/properties listed below. Note that in general, the API largely matches that of the core `@optimizely/optimizely-sdk` client instance, which is documented on the [Optimizely X Full Stack developer docs site](https://docs.developers.optimizely.com/full-stack/docs). The major exception is that, for most methods, user id & attributes are ***optional*** arguments. `ReactSDKClient` has a current user. This user's id & attributes are automatically applied to all method calls, and overrides can be provided as arguments to these method calls if desired.
379426

380-
* `onReady(opts?: { timeout?: number }): Promise` Returns a Promise that fulfills with an object representing the initialization process. The instance is ready when it has fetched a datafile and a user is available (via `setUser` being called with an object, or a Promise passed to `setUser` becoming fulfilled).
427+
* `onReady(opts?: { timeout?: number }): Promise<onReadyResult>` Returns a Promise that fulfills with an `onReadyResult` object representing the initialization process. The instance is ready when it has fetched a datafile and a user is available (via `setUser` being called with an object, or a Promise passed to `setUser` becoming fulfilled). If the `timeout` period happens before the client instance is ready, the `onReadyResult` object will contain an additional key, `dataReadyPromise`, which can be used to determine when, if ever, the instance does become ready.
381428
* `user: User` The current user associated with this client instance
382429
* `setUser(userInfo: User | Promise<User>): void` Call this to update the current user
383430
* `onUserUpdate(handler: (userInfo: User) => void): () => void` Subscribe a callback to be called when this instance's current user changes. Returns a function that will unsubscribe the callback.
@@ -406,7 +453,7 @@ Right now server side rendering is possible with a few caveats.
406453

407454
1. You must download the datafile manually and pass in via the `datafile` option. Can not use `sdkKey` to automatically download.
408455

409-
2. Rendering of components must be completely synchronous (this is true for all server side rendering)
456+
2. Rendering of components must be completely synchronous (this is true for all server side rendering), thus the Optimizely SDK assumes that the optimizely client has been instantiated and fired it's `onReady` event already.
410457

411458
### Setting up `<OptimizelyProvider>`
412459

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@
2929
"dependencies": {
3030
"@optimizely/js-sdk-logging": "^0.1.0",
3131
"@optimizely/optimizely-sdk": "3.6.0-alpha.1",
32+
"@types/react-dom": "^16.9.5",
3233
"hoist-non-react-statics": "^3.3.0",
3334
"prop-types": "^15.6.2",
3435
"utility-types": "^2.1.0"
3536
},
3637
"peerDependencies": {
37-
"react": ">=16.3.0"
38+
"react": ">=16.8.0"
3839
},
3940
"devDependencies": {
4041
"@types/enzyme": "^3.1.15",
@@ -46,8 +47,8 @@
4647
"enzyme": "^3.8.0",
4748
"enzyme-adapter-react-16": "^1.7.1",
4849
"jest": "^23.6.0",
49-
"react": "^16.7.0",
50-
"react-dom": "^16.7.0",
50+
"react": "^16.8.0",
51+
"react-dom": "^16.8.0",
5152
"rollup": "^1.1.0",
5253
"rollup-plugin-commonjs": "^9.2.0",
5354
"rollup-plugin-node-resolve": "^4.0.0",

src/Experiment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class Experiment extends React.Component<ExperimentProps, ExperimentState
5252
this.autoUpdate = !!autoUpdate
5353

5454
if (isServerSide) {
55-
if (optimizely === null) {
55+
if (!optimizely) {
5656
throw new Error('optimizely prop must be supplied')
5757
}
5858
const variation = optimizely.activate(experiment)

src/Feature.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class FeatureComponent extends React.Component<FeatureProps, FeatureState> {
7474
isServerSide,
7575
timeout,
7676
} = this.props
77-
if (optimizely === null) {
77+
if (!optimizely) {
7878
throw new Error('optimizely prop must be supplied')
7979
}
8080

src/autoUpdate.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright 2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { enums } from '@optimizely/optimizely-sdk';
17+
import { LoggerFacade } from '@optimizely/js-sdk-logging';
18+
19+
import { ReactSDKClient } from './client';
20+
21+
interface AutoUpdate {
22+
(
23+
optimizely: ReactSDKClient,
24+
type: 'feature' | 'experiment',
25+
value: string,
26+
logger: LoggerFacade,
27+
callback: () => void
28+
) : () => void
29+
}
30+
31+
/**
32+
* Utility to setup listeners for changes to the datafile or user attributes and invoke the provided callback.
33+
* Returns an unListen function
34+
*/
35+
export const setupAutoUpdateListeners : AutoUpdate = (optimizely, type, value, logger, callback) => {
36+
const loggerSuffix = `re-evaluating ${type}="${value}" for user="${optimizely.user.id}"`;
37+
const optimizelyNotificationId = optimizely.notificationCenter.addNotificationListener(
38+
enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE,
39+
() => {
40+
logger.info(`${enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE}, ${loggerSuffix}`);
41+
callback();
42+
},
43+
);
44+
const unregisterConfigUpdateListener = () => optimizely.notificationCenter.removeNotificationListener(optimizelyNotificationId);
45+
46+
const unregisterUserListener = optimizely.onUserUpdate(() => {
47+
logger.info(`User update, ${loggerSuffix}`);
48+
callback();
49+
});
50+
51+
return () => {
52+
unregisterConfigUpdateListener();
53+
unregisterUserListener();
54+
};
55+
}

src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type OnUserUpdateHandler = (userInfo: UserContext) => void
3030
export type OnReadyResult = {
3131
success: boolean
3232
reason?: string
33+
dataReadyPromise?: Promise<any>
3334
}
3435

3536
const REACT_SDK_CLIENT_ENGINE = 'react-sdk'
@@ -183,6 +184,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
183184
success: false,
184185
reason:
185186
'failed to initialize onReady before timeout, either the datafile or user info was not set before the timeout',
187+
dataReadyPromise: this.dataReadyPromise
186188
})
187189
}, timeout) as any
188190
})

0 commit comments

Comments
 (0)