optimizely / react-sdk

React SDK for Optimizely Feature Experimentation and Optimizely Full Stack (legacy)
https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-react-sdk
Apache License 2.0
89 stars 35 forks source link
optimizely-environment-prod optimizely-environment-public optimizely-owner-px

Optimizely React SDK

This repository houses the React SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).

Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at Optimizely.com, or see the developer documentation.

Optimizely Rollouts is free feature flags for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap.

Get Started

Refer to the React SDK's developer documentation for detailed instructions on getting started with using the SDK.

For React Native, review the React Native developer documentation for installation and implementation detail.

Features

Compatibility

The React SDK is compatible with React 16.8.0 +

Example

import {
  createInstance,
  OptimizelyProvider,
  useDecision,
} from '@optimizely/react-sdk';

const optimizelyClient = createInstance({
  sdkKey: 'your-optimizely-sdk-key',
});

function MyComponent() {
  const [decision] = useDecision('sort-algorithm');
  return (
    <React.Fragment>
      <SearchComponent algorithm={decision.variables.algorithm} />
      { decision.variationKey === 'relevant_first' && <RelevantFirstList /> }
      { decision.variationKey === 'recent_first' && <RecentFirstList /> }
    </React.Fragment>
  );
}

class App extends React.Component {
  render() {
    return (
      <OptimizelyProvider
        optimizely={optimizelyClient}
        timeout={500}
        user={{ id: window.userId, attributes: { plan_type: 'bronze' } }}
      >
        <MyComponent />
      </OptimizelyProvider>
    );
  }
}

Install the SDK

npm install @optimizely/react-sdk

Use the React SDK

Initialization

createInstance

The ReactSDKClient client created via createInstance is the programmatic API to evaluating features and experiments and tracking events. The ReactSDKClient is what powers the rest of the ReactSDK internally.

arguments

returns

import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk';

const optimizely = createInstance({
  datafile: window.optimizelyDatafile,
});

<OptimizelyProvider>

Required at the root level. Leverages React’s Context API to allow access to the ReactSDKClient to the useDecision hook.

props

Readiness

Before rendering real content, both the datafile and the user must be available to the SDK.

Load the datafile synchronously

Synchronous loading is the preferred method to ensure that Optimizely is always ready and doesn't add any delay or asynchronous complexity to your application. When initializing with both the SDK key and datafile, the SDK will use the given datafile to start, then download the latest version of the datafile in the background.

import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk';

const optimizelyClient = createInstance({
  datafile: window.optimizelyDatafile,
  sdkKey: 'your-optimizely-sdk-key', // Optimizely environment key
});

class AppWrapper extends React.Component {
  render() {
    return (
      <OptimizelyProvider optimizely={optimizelyClient} user={{ id: window.userId }}>
        <App />
      </OptimizelyProvider>
    );
  }
}

Load the datafile asynchronously

If you don't have the datafile downloaded, the ReactSDKClient can fetch the datafile for you. However, instead of waiting for the datafile to fetch before you render your app, you can immediately render your app and provide a timeout option to <OptimizelyProvider optimizely={optimizely} timeout={200}>. The useDecision hook returns isClientReady and didTimeout. You can use these to block rendering of component until the datafile loads or the timeout is over.

import { OptimizelyProvider, createInstance, useDecision } from '@optimizely/react-sdk';

const optimizelyClient = createInstance({
  sdkKey: 'your-optimizely-sdk-key', // Optimizely environment key
});

function MyComponent() {
  const [decision, isClientReady, didTimeout] = useDecision('the-flag');
  return (
    <React.Fragment>
      { isClientReady && <div>The Component</div> }
      { didTimeout && <div>Default Component</div>}
      { /* If client is not ready and time out has not occured yet, do not render anything */ }
    </React.Fragment>
  );
}

class App extends React.Component {
  render() {
    return (
      <OptimizelyProvider
        optimizely={optimizelyClient}
        timeout={500}
        user={{ id: window.userId, attributes: { plan_type: 'bronze' } }}
      >
        <MyComponent />
      </OptimizelyProvider>
    );
  }
}

Set user asynchronously

If user information is synchronously available, it can be provided as the user object prop, as in prior examples. But, if user information must be fetched asynchronously, the user prop can be a Promise for a user object with the same properties (id and attributes):

import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk';
import { fetchUser } from './user';

const optimizely = createInstance({
  datafile: window.optimizelyDatafile,
});

const userPromise = fetchUser(); // fetchUser returns a Promise for an object with { id, attributes }

class AppWrapper extends React.Component {
  render() {
    return (
      <OptimizelyProvider optimizely={optimizely} user={userPromise}>
        <App />
      </OptimizelyProvider>
    );
  }
}

useDecision Hook

A React Hook to retrieve the decision result for a flag key, optionally auto updating that decision based on underlying user or datafile changes.

arguments

returns

Render something if flag is enabled

import { useEffect } from 'react';
import { useDecision } from '@optimizely/react-sdk';

function LoginComponent() {
  const [decision, clientReady] = useDecision(
    'login-flag',
    { autoUpdate: true },
    {
      /* (Optional) User overrides */
    }
  );
  useEffect(() => {
    document.title = decision.enabled ? 'login-new' : 'login-default';
  }, [decision.enabled]);

  return (
    <p>
      <a href={decision.enabled ? '/login-new' : '/login-default'}>Click to login</a>
    </p>
  );
}

withOptimizely

Any component under the <OptimizelyProvider> can access the Optimizely ReactSDKClient via the higher-order component (HoC) withOptimizely.

arguments

returns

Example

import { withOptimizely } from '@optimizely/react-sdk';

class MyComp extends React.Component {
  constructor(props) {
    super(props);
    const { optimizely } = this.props;
    const decision = optimizely.decide('feat1');    

    this.state = {
      decision.enabled,
      decision.variables,
    };
  }

  render() {}
}

const WrappedMyComponent = withOptimizely(MyComp);

Note: The optimizely client object provided via withOptimizely is automatically associated with the user prop passed to the ancestor OptimizelyProvider - the id and attributes from that user object will be automatically forwarded to all appropriate SDK method calls. So, there is no need to pass the userId or attributes arguments when calling methods of the optimizely client object, unless you wish to use different userId or attributes than those given to OptimizelyProvider.

useContext

Any component under the <OptimizelyProvider> can access the Optimizely ReactSDKClient via the OptimizelyContext with useContext.

arguments

returns

Example

import React, { useContext } from 'react';
import { OptimizelyContext } from '@optimizely/react-sdk';

function MyComponent() {
  const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
  const decision = optimizely.decide('my-feature');
  const onClick = () => {
    optimizely.track('signup-clicked');
    // rest of your click handling code
  };
  return (
    <>
      { decision.enabled && <p>My feature is enabled</p> }
      { !decision.enabled && <p>My feature is disabled</p> }
      { decision.variationKey === 'control-variation' && <p>Current Variation</p> }
      { decision.variationKey === 'experimental-variation' && <p>Better Variation</p> }
      <button onClick={onClick}>Sign Up!</button>
    </>
  );
}

Tracking

Use the built-in useTrackEvent hook to access the track method of optimizely instance

import { useTrackEvent } from '@optimizely/react-sdk';

function SignupButton() {
  const [track, clientReady, didTimeout] = useTrackEvent()

  const handleClick = () => {
    if(clientReady) {
      track('signup-clicked')
    }
  }

  return (
    <button onClick={handleClick}>Signup</button>
  )
}

Or you can use the withOptimizely HoC.

import { withOptimizely } from '@optimizely/react-sdk';

class SignupButton extends React.Component {
  onClick = () => {
    const { optimizely } = this.props;
    optimizely.track('signup-clicked');
    // rest of click handler
  };

  render() {
    <button onClick={this.onClick}>Signup</button>;
  }
}

const WrappedSignupButton = withOptimizely(SignupButton);

Note: As mentioned above, the optimizely client object provided via withOptimizely is automatically associated with the user prop passed to the ancestor OptimizelyProvider. There is no need to pass userId or attributes arguments when calling track, unless you wish to use different userId or attributes than those given to OptimizelyProvider.

ReactSDKClient

The following type definitions are used in the ReactSDKClient interface:

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 Feature Experimentation developer docs site. 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.

Rollout or experiment a feature user-by-user

To rollout or experiment on a feature by user rather than by random percentage, you will use Attributes and Audiences. To do this, follow the documentation on how to run a beta using the React code samples.

Server Side Rendering

Right now server side rendering is possible with a few caveats.

Caveats

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

  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.

Setting up <OptimizelyProvider>

Similar to browser side rendering you will need to wrap your app (or portion of the app using Optimizely) in the <OptimizelyProvider> component. A new prop isServerSide must be equal to true.

<OptimizelyProvider optimizely={optimizely} user={{ id: 'user1' }} isServerSide={true}>
  <App />
</OptimizelyProvider>

All other Optimizely components, such as <OptimizelyFeature> and <OptimizelyExperiment> can remain the same.

Full example

import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

import {
  createInstance,
  OptimizelyProvider,
  useDecision,
} from '@optimizely/react-sdk';

const fetch = require('node-fetch');

function MyComponent() {
  const [decision] = useDecision('flag1');
  return (
    <React.Fragment>
      { decision.enabled && <p>The feature is enabled</p> }
      { !decision.enabled && <p>The feature is not enabled</p> }
      { decision.variationKey === 'variation1' && <p>Variation 1</p> }
      { decision.variationKey === 'variation2' && <p>Variation 2</p> }
    </React.Fragment>
  );
}

async function main() {
  const resp = await fetch('https://cdn.optimizely.com/datafiles/<Your-SDK-Key>.json');
  const datafile = await resp.json();
  const optimizelyClient = createInstance({
    datafile,
  });

  const output = ReactDOMServer.renderToString(
    <OptimizelyProvider optimizely={optimizelyClient} user={{ id: 'user1' }} isServerSide={true}>
      <MyComponent />
    </OptimizelyProvider>
  );
  console.log('output', output);
}
main();

Disabled event dispatcher

To disable sending all events to Optimizely's results backend, use the logOnlyEventDispatcher when creating a client:

import { createInstance, logOnlyEventDispatcher } from '@optimizely/react-sdk';

const optimizely = createInstance({
  datafile: window.optimizelyDatafile,
  eventDispatcher: logOnlyEventDispatcher,
});

Additional code

This repository includes the following third party open source code:

hoist-non-react-statics Copyright © 2015 Yahoo!, Inc. License: BSD

js-tokens Copyright © 2014, 2015, 2016, 2017, 2018, 2019 Simon Lydell License: MIT

json-schema Copyright © 2005-2015, The Dojo Foundation License: BSD

lodash Copyright © JS Foundation and other contributors License: MIT

loose-envify Copyright © 2015 Andres Suarez zertosh@gmail.com License: MIT

node-murmurhash Copyright © 2012 Gary Court, Derek Perez License: MIT

object-assign Copyright © Sindre Sorhus (sindresorhus.com) License: MIT

promise-polyfill Copyright © 2014 Taylor Hakes Copyright © 2014 Forbes Lindesay License: MIT

prop-types Copyright © 2013-present, Facebook, Inc. License: MIT

react-is Copyright © Facebook, Inc. and its affiliates. License: MIT

react Copyright © Facebook, Inc. and its affiliates. License: MIT

scheduler Copyright © Facebook, Inc. and its affiliates. License: MIT

utility-types Copyright © 2016 Piotr Witek piotrek.witek@gmail.com License: MIT

node-uuid Copyright © 2010-2016 Robert Kieffer and other contributors License: MIT

To regenerate the dependencies use by this package, run the following command:

npx license-checker --production --json | jq 'map_values({ licenses, publisher, repository }) | del(.[][] | nulls)'

Contributing

Please see CONTRIBUTING for more information.

Credits

First-party code subject to copyrights held by Optimizely, Inc. and its contributors and licensed to you under the terms of the Apache 2.0 license.

Other Optimizely SDKs