react-splitz
v2.0.4
Published
A/B & Multivariate Testing library for React projects
Downloads
7
Maintainers
Readme
React Splitz 📊📈
React Splitz is an A/B and multivariate testing tool for react applications.
Easy way to set and manage persistent experiments in your app and make careful changes to your user experiences while collecting data on the results. Subscribe and access experiments from any component via component props or hooks that we all know and love. It also offers full support for server-side rendering so you're able to use it in your universal web apps as well. Very performant and lightweight (~ 3.9kb minified/gzipped).
Installation
Using npm:
npm install react-splitz --save
Using yarn:
yarn add react-splitz
or UMD build (ReactSplitz
as the global variable)
// Development build
<script src="https://unpkg.com/react-splitz@2/umd/react-splitz.development.js"></script>
// Production build
<script src="https://unpkg.com/react-splitz@2/umd/react-splitz.production.min.js"></script>
Setup
First we have to import our <TestContainer /> wrapper component to wrap around our app/top-level component. All configuration for experiments will be provided as props to this component.
Note: It is essential that we wrap our component around our top level application component so that experiments and chosen experiment buckets are accessible throughout the entire application.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// Import TestContainer from our package
import { TestContainer } from 'react-splitz';
/**
* Define your experiments in an array like this. In a real
* application it might make more sense to move this array to a config file
* or retrieve it from an API and import it here.
**/
const experiments = [
{
name: 'button-size',
variants: [
{ name: 'default' },
{ name: 'small' },
{ name: 'medium' },
{ name: 'large' },
],
},
{
name: 'signup-btn-color',
variants: [
{ name: 'default' },
{ name: 'gray' },
{ name: 'blue' },
{ name: 'red' },
],
},
];
ReactDOM.render(
<TestContainer
// Pass experiments array as a prop
experiments={experiments}
>
<App />
</TestContainer>,
document.getElementById('root')
);
Now you can access running experiments from any component with the withTest
wrapper component. In this case, we wrap the SignupButton
component with it.
SignupButton.js
import React, { Component } from 'react';
// Import 'withTest' wrapper here
import { withTest } from 'react-splitz';
class SignupButton extends Component {
render() {
/**
* Each of the experiment props provide a `variant` key which
* is the chosen variant name of the experiment.
**/
const { expButtonColor, expButtonSize } = this.props;
const classes = ['signup-btn']
if (expButtonColor.variant) { // i.e 'red'
classes.push(`button-${expButtonColor.variant}`) // button-red
}
if (expButtonSize.variant) { // i.e 'medium'
classes.push(`button-${expButtonSize.variant}`) // button-medium
}
<button className={classes.join(' ')}>
{'Sign Up'}
</button>
}
}
// Map the experiments to prop names for your SignupButton component.
// The value of the prop will be an object with information about the chosen variant.
const mapExperimentsToProps = {
expButtonColor: 'signup-btn-color',
expButtonSize: 'button-size'
}
export withTest(mapExperimentsToProps)(SignupButton)
Using hooks
Instead of using the withTest
Higher Order component (HOC) you can use the useExperiment
hook to subscribe to an experiment. Here's an example.
// Import 'useExperiment' hook here
import { useExperiment } from 'react-splitz';
function MyComponent(props) {
const { variant } = useExperiment('signup-btn-color');
// Use variant here to decide how to markup your UI
}
Note: Hooks require React version 16.8 or later
Weighted Experiments
You can assign weights to experiment buckets by adding weight values in decimal values to each variant object equalling 1.
Note: Decimals values for weights have to be multiples of 0.10. (0.3, 0.2 etc)
const experiments = [
{
name: 'download-promo',
variants: [
// 30% (popup) + 20% (button) + 50% (fixed) = %100
{ name: 'popup', weight: 0.3 },
{ name: 'button', weight: 0.2 },
{ name: 'fixed', weight: 0.5 },
],
},
];
User-based / Cross-device tracking
You can pass the identifier
option to an experiment to ensure that the same user is always directed to the same bucket. This should be a unique identifier like a user id, user token, etc.
const experiments = [
{
name: 'download-promo',
variants: [{ name: 'popup' }, { name: 'button' }, { name: 'fixed' }],
identifier: 'lk2ds89', // User id
},
];
Forcing Specific Variants
This option can come in handy for situations where you want to easily test a specific variant during development or quickly switch all your user traffic to a winning variant of an experiment without having to standardize the feature in your codebase right away.
All you have do is pass in the forcedExperments
object prop to the <TestContainer /> component with the experiment you want to override and the variant you want to override it with.
const experiments = [
{
name: 'download-promo',
variants: [
{ name: 'popup' },
{ name: 'button' },
{ name: 'fixed' }
]
}
]
const forcedExperiments = {
'download-promo': 'button' // This will force variant 'button' to be set.
}
<TestContainer
experiments={experiments}
forcedExperiments={forcedExperiments}
>
<App />
</TestContainer>
Get All Running Experiments
You can pass the callback function prop getExperiments
to the <TestContainer /> component which gets called once experiments are set. This function will get called with an object of currently running experiments.
Example
const getExperiments = experiments => console.log(experiments);
Disable All Experiments
If you'd like to disable all experiments at any time, pass in the disableAll
prop to TestContainer
and set it to true
.
Server-side Rendering
Server-side rendering setup for Universal JS apps is set up the same way as a client-side only web app with the addition of 2 extra required function props. You have to provide the getCookie
and setCookie
function props to the <TestContainer /> component as ways to retrieve and set cookies on the server.
Make sure you also wrap your client-side application with TestContainer as well like the examples shown above.
Here's an SSR example using Express.js
import express from 'express';
import cookieParser from 'cookie-parser';
import React from 'react';
// Import TestContainer wrapper
import { TestContainer } from 'react-splitz';
const app = express();
app.use(cookieParser());
// Define experiments
const experiments = [
{
name: 'button-size',
variants: [
{ name: 'default' },
{ name: 'small' },
{ name: 'medium' },
{ name: 'large' },
],
},
{
name: 'signup-btn-color',
variants: [
{ name: 'default' },
{ name: 'gray' },
{ name: 'blue' },
{ name: 'red' },
],
},
];
app.get('/', (req, res) => {
res.send(
<TestContainer
experiments={experiments}
getCookie={x => req.cookies[x]} // Retrieve cookie on server
setCookie={(x, y) => res.cookie(x, y)} // Set cookie on server
>
<App />
</TestContainer>
);
});
app.listen(3000);
Tracking
Using a tracking service like Mixpanel or Goole Analytics to track running experiments is very easy. You have access to running experiments either by using the getExperiments
callback that fires when your application is loading or you can subscribe to individual experiments using the withTest
wrapper with your components. With that you are able to tailor how and when you make calls to these tracking services based on experiments running.
Update Experiment Variants Instantly
You are able to update experiments to different variations/buckets on the fly without reloading your app with an option available through the withTest
wrapper.
Use case: Building a configuration dashboard to view and manage all running experiments for local development or staging environments. You can add functionality to quickly switch between variants/buckets and have them update instantly throughout your entire application. See more on how to use this feature below in API Reference
Composability
The withTest
higher-order component is fully composable with other higher-order components (HOCs) in your app.
So if you were using this with connect
from Redux or withRouter
from React Router, for example, you could do something like this.
const EnhancedComponent = connect(mapStateToProps)(
withRouter(withTest(mapExpsToProps)(WrappedComponent))
);
Although this code works, it makes things a bit messy and hard to follow. Because of this we would recommend using a composition utility function to make things clearer to read. With that you can rewrite it as:
const enhance = compose(
connect(mapStateToProps, null),
withRouter,
withTest(mapExpsToProps),
... // Other HOCs
)
const EnhancedComponent = enhance(WrappedComponent);
The compose
utility function is provided by many third-party libraries including lodash (as lodash.flowRight
), Redux, and Ramda.
API Reference
<TestContainer />
option props
experiments
- (Array [required]): Running experiments. See above for examples.disableAll
- (Boolean [optional]): Option to isable all experiments. More info here Disable All Experiments.forcedExperiments
- (Object [optional]): An object with experiment key/values as experiment name/forced variant. More info here: Forcing specific variantsgetCookie
- (Function [optional]): A function prop provided to retrieve a browser cookie. Required for server-side rendering.setCookie
- (Function [optional]): A function prop provided to set a browser cookie. Required for server-side rendering.
withTest([mapExperimentsToProps], [options])([WrappedComponent])
[
WrappedComponent
] - (Function or Class): The wrapped component you want subscribed to experiments. Note: Observe above that this argument is passed in during the second function invocation.[
mapExperimentsToProps
] - (Object): Experiments you want to subscribe to usingkey/value
pairs. The objectkey
will be the what you want to name the component prop for the experiment. Thevalue
you provide will be the experiment name. chosen variant/bucket information. More info on the value below[
options
] - (Object [optional]): An object that you can pass in as the third argument for extra functionality. Options are below:[
updateExperiments
] - (Boolean): Allows you to update an experiment's variant/bucket. If this option is provided and set totrue
, you will have access to anupdateExperiments()
function prop in your wrapped components that allows you to update experiments.this.props.updateExperiments(<UpdatedExperiments>, <callback>)
takes in an object as the first argument withkey/value
pairs. The experiment name being thekey
and the variant being thevalue
. Second argument is an optional callback function you can set that will fire after experiments have been updated.Example
// Updating the 'button-size' experiment with variant 'large' this.props.updateExperiments({ 'button-size': 'large', });
Experiment Prop(s) returned from [mapExperimentsToProps]
Each experiment object prop comes with the following keys.
variant
(String) - Chosen variant name for the experiment.