1.11.0 • Published 17 days ago

@splitsoftware/splitio-redux v1.11.0

Weekly downloads
938
License
Apache-2.0
Repository
github
Last release
17 days ago

Redux SDK Library

A library to easily use Splitio JS SDK with Redux and React Redux. It provides helper functions over Splitio JS SDK, so that you don't need to directly access the SDK factory and client.

INTERNAL COMMENT: several library features were adapted from the Webconsole project.

Get Started

Run examples

The /examples folder includes two working apps with Redux and React-Redux:

$ npm install
$ npm run build
$ cd examples/react-redux-spa
/examples/react-redux-spa$ yarn install
/examples/react-redux-spa$ yarn start
...
$ cd examples/react-redux-ssr
/examples/react-redux-ssr$ npm install
/examples/react-redux-ssr$ npm run dev

Run in Webconsole REMOVE BEFORE PUBLISHING

As a PoC and for testing purposes, the library was integrated to the Admin and Login apps of the Webconsole project, and partially integrated to the Main app.

Follow the steps below to run it:

$ npm run build
$ npm link
...
/webconsole$ git fetch
/webconsole$ git checkout sdk_redux_library_migration
/webconsole$ npm link react-sdk-library
/webconsole$ npm start
/webconsole$ npm test

Usage and API

Installation

NPM

$ npm install redux redux-thunk react-redux redux-sdk-library

Yarn

$ yarn add redux redux-thunk react-redux redux-sdk-library

Note: react-redux is only necessary for some features of the library. Check them in section Usage with React Redux.

Create store

To use the library, we need to pass the splitReducer to the store and the Thunk middleware to handle async actions.

A first action must be dispatched to initialize the Splitio SDK, invoking the initSplitSdk action creator.

Below is a simple example for creating a redux store:

// Import redux and redux-sdk-library
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk'
import { initSplitSdk, reducer as splitReducer } from 'redux-sdk-library'

// Instantiate the store
const storeInstance = createStore(
  combineReducers({
    splitio: splitReducer
    /* You'll have your app reducers here too. */
  }),
  // The library uses async actions that require Thunk middleware
  composeEnhancers(applyMiddleware(thunk))
);

// Split config, with the same format than the one provided to Split factory.
const splitBrowserConfig = {
  core: {
    authorizationKey: 'YOUR_BROWSER_API_KEY',
    key: 'user_key'
  }
};

// Dispatch action to initilize Splitio SDK
storeInstance.dispatch(initSplitSdk(splitBrowserConfig));

Note: initSplitSdk is an async action creator that initialize the SDK. For scenarios where more than one store is created, such as server rendering, invoke initSplitSdk once and dispatch its result on each new store:

Example:

const splitServerConfig = {
  core: {
    authorizationKey: 'YOUR_SDK_API_KEY'
  }
};

const asyncAction = initSplitSdk(splitServerConfig);

const app = express();

app.get('*', (req, res) => {
  // A new store is created per request
  const store = createStore(reducers, initialState, applyMiddleware(thunk));
  store.dispatch(asyncAction);
  ...
}

Split state

The Split reducer updates a piece of state with the following shape:

{
  // 'splitio' is the key where the Split reducer is mounted
  'splitio': {

    // The following properties indicates the current status of the SDK
    'isReady': true; // boolean indicating if the SDK has triggered an SDK_READY event
    'isTimedout': false; // boolean indicating if the SDK has triggered an SDK_TIMEDOUT event
    'lastUpdate': 12312312312; // timestamp of the last SDK event (SDK_READY, SDK_TIMEDOUT or SDK_UPDATED)

    /* The 'treatments' property contains the evaluations of Splits.
     * Each evaluation is associated with an Split name and a key (e.g., user id or organization name).
     * Thus the property has 3 levels: split name, split key, and finally the treatment that was evaluated for that split and key.
     */
    'treatments': { // First level: list of evaluated splits
      'split_name_1': { // Second level: list of evaluated keys for the container split
        'user_key': { // Third level: evaluated treatment, formed by a 'treatment' value and a config that might be null
          'treatment': 'on',
          'config': '{ ... }'
        },
        'org_key': {
          'treatment': 'on',
          'config': '{ ... }'
        }
      },
      'split_name_2': {
        'user_key': {
          'treatment': 'treatment_A',
          'config': null
        }
      }
    }
  }
}

INTERNAL COMMENTS:

  • On most escenarios on browser, Splitio SDK is used with a single key. Thus a second reducer could be implemented that considers a treatments property without the second level.
  • The treatments property could be structured with the levels split key -> split name -> treatment instead of split name -> split key -> treatment.
  • Treatments at the 3rd level could be normalized putting them outside the treatments property, in order to reduce redundancy (different keys for the same split could evaluate to the same treatment).
  • The possibility of multiple factories is not contemplated for the MVP.
  • Treatment configs could be deserialized into JS objects before storing them. However, keeping them as strings makes it easier to compare treatments for equality inside the reducer.

Action creators

TODO: DOCUMENT ACTION CREATORS FOR SERVER-SIDE

initSplitSdk

This action creator initializes the Splitio SDK. It accepts a configuration param and returns a Thunk (async) action.

function initSplitSdk: (params: {
  config: IBrowserSettings;
  onReady?: () => any;
  onTimedout?: () => any;
  onUpdate?: () => any;
}) => ThunkAction

config: setting object used to initialize the Split factory.

onReady, onTimedout and onUpdate are optional callback functions that will be invoked on SDK_READY, SDK_READY_TIMEDOUT and SDK_UPDATE events respectively.

getTreatments

This action creator performs a treatment evaluation, i.e., it invokes the actual client.getTreatment* methods. It accepts a param with the different objects required for an evaluation, and returns either a plain action (FSA) with the result of the evaluation, or an async action if the SDK is not ready yet.

function getTreatments: (params: {
  key?: SplitKey;
  splitNames: string[] | string;
  attributes?: Attributes;
  evalOnUpdate?: boolean;
}) => ThunkAction | FluxStandardAction

key: optional split key. If not provided, it defaults to the key defined in the SDK setting, i.e., the config object passed to initSplitSdk.

splitNames: split name or array of split names to evaluate.

attributes: optional map of attributes passed to the actual client.getTreatment* methods.

evalOnUpdate: indicates to re-evaluate the splits if the SDK is updated. For example, a true value might be the desired behaviour for permission toggles or operation toggles, such as a kill switch, that you want to inmediately reflect in your app. A false value might be useful for experiment or release toggles, where you want to keep the treatment unchanged during the sesion of the user. The param is true by default.

Helper functions and selectors

TODO: DOCUMENT TRACK FOR SERVER-SIDE

track

This function track events, i.e., it invokes the actual client.track* methods. It accepts a param with the different objects required for an event tracking, and returns a boolean value indicating whether or not the SDK was able to successfully queue the event to be sent back to Split servers.

function track: (params: {
    key?: SplitKey;
    trafficType?: string;
    eventType: string;
    value?: number;
    properties?: Properties;
}) => boolean

key: optional split key. If not provided, it defaults to the key defined in the SDK config object.

trafficType: the traffic type of the key in the track call. If not provided, it defaults to the traffic type defined in the SDK config object. If not provided either in the SDK setting, the function logs an error message and returns false.

eventType, value and properties follow the same rules then the client.track* method params.

getSplitNames

Returns the names of Splits registered with the SDK. The array might be empty if the SDK was not initialized (by dispatching a "initSplitSdk" action) or if it's not ready yet.

function getSplitNames(): string[]

selectTreatmentValue and selectTreatmentWithConfig

These functions extract a treatment evaluation from the Split state. The first one returns the treatment string value, and the second one a treatment object containing its value and configuration. See Get Treatments with Configurations for details.

function selectTreatmentValue(splitState: ISplitState, splitName: string, key?: SplitKey, defaultValue?: string): string

function selectTreatmentWithConfig(splitState: ISplitState, splitName: string, key?: SplitKey, defaultValue?: TreatmentWithConfig): TreatmentWithConfig

splitState: Split piece of state from the store.

splitName: split name.

key: optional split key. If not provided, it returns the first evaluation done for splitName.

defaultValue: the value to return if the treatment is not found in the store (the getTreatment evaluation was not performed for the given split and key). It defaults to 'control' for the first selector, and { treatment: 'control', config: null} for the second.

INTERNAL COMMENTS:

  • The configs of treatments are stored as JSON strings, but we could deserialize them into JS objects.
  • The actionToggler function in Webconsole could be also included as helper function.

Usage with React Redux

The library provides some functions to connect your React components to Split pieces of state and functionality. These functions use the connect() method, thus you need to include react-redux in the project.

mapTreatmentToProps

function mapTreatmentToProps(splitName: string, key?: SplitKey, getSplitState?: IGetSplitState): (state: any) => { feature: string }

This function returns a mapStateToProps function to be used with connect. As a result, a string property named feature will be merged as prop for the connected component. Its value will be the treatment value evaluated for the given splitName and key.

getSplitState is a optional function that takes the entire Redux state and returns the state slice which corresponds to where the Split reducer was mounted. This functionality is rarely needed, and defaults to assuming that the reducer is mounted under the splitio key.

Example:

import { mapTreatmentToProps } from 'redux-sdk-library';

export default connect(mapTreatmentToProps('split_name'))(component);

mapIsFeatureOnToProps

function mapIsFeatureOnToProps(splitName: string, key?: SplitKey, getSplitState?: IGetSplitState): (state: any) => { isFeatureOn: string }

This function also returns a mapStateToProps function to be used with connect. As a result, a boolean property named isFeatureOn will be merged as prop for the connected component. Its value will be true if the treatment for the given splitName and key evaluated to 'on', or false otherwise.

getSplitState is a optional function that takes the entire Redux state and returns the state slice which corresponds to where the Split reducer was mounted. This functionality is rarely needed, and defaults to assuming that the reducer is mounted under the splitio key.

Example:

import { mapIsFeatureOnToProps } from 'redux-sdk-library';

export default connect(mapIsFeatureOnToProps('split_name'))(component);

connectToggler

function connectToggler(splitName: string, key?: SplitKey, getSplitState?: IGetSplitState):
  (ComponentOn: React.ComponentType, ComponentOff: React.ComponentType ) => React.ComponentType

The connectToggler connector returns a HOC function given a splitName, and an optional key and getSplitState function.

The returned HOC accepts two components as input and returns a wrapper component that will render ComponentOn component if the treatment for the given split name and key is 'on', or ComponentOff component otherwise.

If key is not provided, the treatment value of the first evaluation of splitName split is considered.

getSplitState is a function that takes the entire Redux state and returns the state slice which corresponds to where the Split reducer was mounted. This param defaults to assuming that the reducer is mounted under the splitio key.

Example:

import { connectToggler } from 'redux-sdk-library';

const ConnectedFeatureTogler = connectToggler(SPLIT_NAME)(FeatureComponent, LegacyComponent);

connectSplit

function connectSplit(getSplitState?: IGetSplitState):
  (component: React.ComponentType) => React.ComponentType

This decorator connects your components with:

  • The Split state at Redux, under the prop key split. See Split state for details.
  • The action creator getTreatments, binded to the dispatch of your store. See getTreatments for details.

Example:

import { connectSplit, selectTreatmentValue } from 'redux-sdk-library';

class MyComponent extends React.Component {

  componentDidMount() {
    this.props.getTreatments({ splitNames: 'my_split' });
  };

  renderOn() { ... }

  renderOtherwise() { ... }

  render() {
    selectTreatmentValue(this.props.split, 'my_split') === 'on' ?
      renderOn() : renderOtherwise();
  }
};

export default connectSplit()(myComponent);

TODO

  • Define library name
  • Improve build configs and project structure
  • Tests
  • Polish examples
  • Code documentation with JSDoc format
  • Review performance of reducer
  • Missing feature from Webconsole: actionToggler

License

Licensed under the Apache License, Version 2.0. See: Apache License.

About Split

Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery.

To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup.

Split has built and maintains SDKs for:

For a comprehensive list of open source projects visit our Github page.

Learn more about Split:

Visit split.io/product for an overview of Split, or visit our documentation at help.split.io for more detailed information.

1.11.1-rc.0

17 days ago

1.11.0

1 month ago

1.10.1-rc.4

1 month ago

1.10.1-rc.1

1 month ago

1.10.1-rc.2

1 month ago

1.10.1-rc.0

1 month ago

1.10.1-rc.3

1 month ago

1.9.1-rc.1

5 months ago

1.9.1-rc.0

5 months ago

1.10.0

5 months ago

1.9.0

10 months ago

1.8.0

10 months ago

1.7.4-rc.2

10 months ago

1.7.4-rc.0

10 months ago

1.7.4-rc.1

10 months ago

1.7.3

12 months ago

1.7.3-rc.0

12 months ago

1.7.2-rc.0

1 year ago

1.7.2-rc.1

1 year ago

1.7.2

1 year ago

1.7.1

1 year ago

1.7.0

2 years ago

1.7.1-rc.0

1 year ago

1.7.1-rc.1

1 year ago

1.6.1-rc.0

2 years ago

1.6.0

2 years ago

1.5.1-rc.2

2 years ago

1.5.1-rc.1

2 years ago

1.5.0

2 years ago

1.4.1-rc.2

2 years ago

1.4.1-rc.0

2 years ago

1.4.1-rc.1

2 years ago

1.4.0

3 years ago

1.3.3-rc.0

3 years ago

1.3.2

3 years ago

1.3.2-canary.0

3 years ago

1.3.1

3 years ago

1.3.0

3 years ago

1.2.2-canary.0

3 years ago

1.2.1-canary.0

4 years ago

1.2.1

4 years ago

1.2.1-canary.1

4 years ago

1.2.0

4 years ago

1.2.0-canary.0

4 years ago

1.1.0

4 years ago

1.1.0-canary.1

4 years ago

1.1.0-canary.0

4 years ago

1.0.1

4 years ago

1.0.1-canary.0

4 years ago

1.0.0

4 years ago

0.7.0

4 years ago

0.1.0-rc.8

4 years ago

0.6.0

4 years ago

0.1.0-rc.7

4 years ago

0.1.0-rc.6

4 years ago

0.5.0

4 years ago

0.1.0-rc.5

4 years ago

0.1.0-rc.4

4 years ago

0.1.0-rc.3

4 years ago

0.1.0-rc.2

4 years ago

0.1.0-rc.1

4 years ago

0.1.0-rc.0

4 years ago