These toggles are then corner stone of the Continuous Delivery cycle, they are short lived and mostly for development purposes.
The idea is to wrap all development of the next release in to a toggle (i.e. v2.0.2
), this way
development can carry on while been able to still deploy to production using the same branch.
Once the release is live and has been integrated on production, all the conditionals and the whole of the release toggle is removed and all functionality integrated as part of the normal codebase, then the toggle for the next release is created.
Its ok to use lots of ifs on the implementation of these since they will be removed on the short term.
Also known as multivariant or A/B Testing, these are created to experiment with different variations of a UI element, these are generally mid to short lived and they are for marketing or UX purposes.
These toggles will always be implemented with a default or the existing behaviour as a default, and then other variations will appear instead if the toggle is active, the best way to implement these is using whenActive
as we'll see later on.
When a decision has been made on which version performs best the toggle will be removed along with any discarded variations.
This toggles are used when the business or operations wants a certain feature to be able to be deactivated or activated at any time.
This could include something like the ability to deactivate Nectar services or certain pages from appearing, or also something like the way the Node layer behaves on production.
These toggles can be long lived and used for business purposes as well as operational purposes.
Since these toggles can be on the codebase for a long time, the implementation should be more careful and future proof, ideally applying ideas of inversion of control or factories to avoid writing lots of conditionals everywhere.
The implementation on the frontend will be quite straight forward.
The toggle configuration is loaded on the ToggleStore
on initialisation and could (albeit not implemented) be changed on runtime.
The toggles live in an Immutable.Map
with the toggle name as the key and a boolean as the value, true
for active toggles, false
for inactive.
There are two utilities created to make the implementation quite easy, the first one is the withToggles
util which is meant for components.
Important: To make easier the later removal of toggles, we're going to add a comment above the line every time we use a toggle, for example // TOGGLE: toggle-name
. That will make it easier to search for all checks of that toggle and remove them quicker.
It wraps a given component into another component which loads and listens for changes on the ToggleStore
and adds two methods on the props of the child component, isActive
and whenActive
. They're used like this:
const withToggles = require( '../utils/withToggles' );
const MyComponent = React.createClass({
...
someMethod() {
// TOGGLE: some-toggle
if ( this.props.isActive( 'some-toggle' ) ) {
return somethingElse;
}
return something;
}
...
render() {
// TOGGLE: variation-b
return this.props.whenActive( 'variation-b', MyComponentB, MyComponentA );
}
});
module.exports = withToggles( MyComponent );
The isActive
method accepts a toggle name, and returns a boolean.
The whenActive
method accepts a toggle name as the first argument, the second argument is whatever you want to return if the toggle is active, and the third argument is whatever you want to return if the toggle is not active. If the third argument is not passed it returns null
if the toggle is not active.
This implementation also makes it really easy to test, since you can just pass your toggle preferences by props to the component you're testing as we'll see later on.
The second utility is meant to be used anywhere else other than a component, for example on services, stores, etc.
It also feeds from the ToggleStore
(but without subscribing) and it exposes the same two methods, isActive
and whenActive
with the same functionality as the withToggles
methods.
Some examples of use:
const { isActive, whenActive } = require( '../util/toggles' );
...
// TOGGLE: my-toggle
if ( isActive( 'my-toggle' ) ) {
//do something
}
...
// TOGGLE: my-other-toggle
return whenActive( 'my-other-toggle', this.someMethod, this.someOtherMethod );
...
// TOGGLE: some-toggle
let myObject = whenActive( 'some-toggle', objectB, object );
...
// TOGGLE: a-toggle
let response = whenActive( 'a-toggle', purefunctionA, purefunctionB );
...
// TOGGLE: string-toggle
let title = whenActive( 'string-toggle' , 'Title B', 'Title A' );
Testing components with different toggle configuration is pretty easy.
The first thing to do for consistency and later integration is to wrap your toggle tests on a different describe
suite.
Then you can pass a mocked function of whichever utility function you use on the component through props:
//This component uses isActive
let active = () => true;
let inactive = () => false;
describe( 'My component' () => {
it ( 'should render as always', () => {
let component = mount( <MyComponent isActive={ inactive } /> );
});
...
describe( 'TOGGLE: my-toggle', () => {
it ( 'should show something different when my-toggle is active', () => {
let component = mount( <MyComponent isActive={ active } /> );
...
});
});
});
Testing other code is also similarly easy, there is a mocked version of toggles.js
already in place that you can use like this:
const toggles = require( '../util/toggles' );
toggles._setToggles({
'some-toggle': false
});
describe( 'MyService', () => {
it ( 'should work as usual', () => {
...
});
describe( 'TOGGLE: service-toggle', () => {
beforeEach( () => {
toggles._setToggles({
'some-toggle': true
});
});
it ( 'should do something special now', () => {
...
});
});
});
There are some ways to avoid writing too many conditionals when writing a new functionality, here are some rules of thumb to consider.
As a general rule, if you can extract the needed new functionality into a new method that doesn't affect the rest of the code and avoid conditionals, do it that way.
-
If the new feature changes the fundamental way a component works or the refactor makes it extremely difficult to make it backwards compatible, write a new component with a different variation name and use
whenActive
to load it conditionally on the parent. When/if the feature is released you can get rid of the old component all together. -
If the new feature requires a brand new component with no backup, use
whenActive
without third argument to render it:{ this.props.whenActive( 'toggle', <BrandNewComponent /> ) }
.
-
If its a new service/store, implement it directly on a new file.
-
If its an existing service/store which adds new functionality, add new methods and use them only on the pertinent components, it shouldn't need any conditionals.
-
If its an existing service/store which needs a refactor on an existing functionality but can still be backwards compatible, use toggle conditionals.
-
If its an existing service/store which needs a refactor that makes it break backwards compatibility, duplicate the file into a new file where you can do the refactor, then use it conditionally where needed. When the feature is release you can get rid of the old code.
This is an area to explore still. Right now the toggles are controlled by a simple json file on server/toggles/index.js
, they are loaded on start up time and stay like that until changed and reloaded.
Ideally we would want more flexibility around that.
The easiest, fastest way to control the toggles with our current architecture would be to use Consul's key-value storage to control the values of the toggles on each environment without requiring a code change. The config could be loaded at start-up time, or in a time interval (i.e. each hour).
More advanced and bespoke dashboards exist to control feature toggles, for example The Finantial Times has open sourced a hosted API and dashboard solution called next-flags-api.
There's also Flip which also allows you to use cookies or DB to enable toggles per user or general.
Other options include the use of SaaS solutions such as LaunchDarkly or Optimizely (this last one mainly only for A/B testing).