UNPKG

redux-ab-test

Version:

A/B testing React components with Redux and debug tools. Isomorphic with a simple, universal interface. Well documented and lightweight. Tested in popular browsers and Node.js. Includes helpers for React, Redux, and Segment.io

130 lines (119 loc) 4.65 kB
import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import selectVariation from '../../utils/select-variation'; import { logger } from './logger'; import LoadedComponent from './loaded-component'; import { groupChildrenByName, groupExperimentsByName, requireChildrenAreVariations, } from './selectors'; class Experiment extends React.Component { static propTypes = { // Name of the experiment name: React.PropTypes.string.isRequired, defaultVariationName: React.PropTypes.string.isRequired, // Selector for the experiment isEnabled: React.PropTypes.func.isRequired, // React element to render: children: requireChildrenAreVariations, // Optional selectors for additional control isLoading: React.PropTypes.func, forceRender: React.PropTypes.bool, // Optional components for loading state: getLoadingState: React.ProptTypes.func, getDisabledState: React.ProptTypes.func, // // Redux store props: // experimentsByName: React.PropTypes.object.isRequired, reduxAbTest: ImmutablePropTypes.map.isRequired, }; static defaultProps = { isLoading: () => false, getLoadingState: null, getDisabledState: null, forceRender: false, }; render() { const { name, defaultVariationName, isEnabled, isLoading, experimentsByName, reduxAbTest, children } = this.props; const enabled = isEnabled(); const loading = isLoading(); const childrenByName = groupChildrenByName(children); logger(`${__filename}: Rendering Experiment name='${name}', enabled='${enabled}', loading='${loading}', children.names=${Object.keys(childrenByName)}`); // The default output is default variation's children, // when the experiment is disabled / loading / invalid. const defaultOutput = childrenByName[defaultVariationName] && childrenByName[defaultVariationName].children; // // Render the disabled / loading states // if (!enabled) { logger(`${__filename}: Experiment name='${name}' is disabled`); const { getDisabledState } = this.props; if (getDisabledState) { return getDisabledState(); } return <span>{defaultOutput}</span>; } if (loading) { logger(`${__filename}: Experiment name='${name}' is loading`); const { getLoadingState } = this.props; if (getLoadingState) { return getLoadingState(); } return <span>{defaultOutput}</span>; } // // Get the experiment && variation from the input props: // let experiment = null; let variation = null; experiment = experimentsByName[name]; variation = experiment && selectVariation({ experiment: experiment, active: reduxAbTest.get('active'), defaultVariationName: defaultVariationName, }); if (!experiment && forceRender) { // Force the experiment object into existance to allow the component to render // This should be used for ad-hock experiment logger(`${__filename}: Experiment name='${name}' is forced to render`); experiment = Immutable.Map({ name }); variation = Immutable.Map({ name: defaultVariationName }); } if (!experiment) { logger(`${__filename}: Experiment name='${name}' is enabled, but not in the store`); return <span>{defaultOutput}</span>; } if (!variation) { logger(`${__filename}: Experiment name='${name}' is enabled, but no variation is available`); return <span>{defaultOutput}</span>; } const variationName = variation.get('name', ''); // Valdiate the variation selected exists if (!childrenByName[variationName]) { logger(`${__filename}: Experiment name='${name}' has no child with the name='${variationName}'`); return <span>{defaultOutput}</span>; } // // Render the component wrapper with the attached experiment // logger(`${__filename}: Experiment name='${name}' is enabled`); return ( <LoadedComponent experimentName={name} variationName={variationName} experiment={experiment} variation={variation} > {children} </LoadedComponent> ); } } export const mapStateToProps = (state) => ({ reduxAbTest: state, experimentsByName: groupExperimentsByName(state), }); export default connect(mapStateToProps)(Experiment);