Feature Flags in React
A month ago I gave a talk at js.talks() conference about React at Product Hunt.
One of the sections, which didn't make it in the talk, was how feature flags are handled in Product Hunt.
Almost every feature in Product Hunt starts with a feature flag in Flipper. The feature is only available for selected group of users. Initially only for the developers working on it. This allows for splitting big feature into smaller deployable chunks. This eliminates large list of problems and allows for very early feedback on features.
The usual feature timeline looks something like:
After a feature is completed and had run without issues for some time the feature flag is removed.
Working in such way, also helps with code structure. Since all features are isolated.
Usage
In the backend, there is a facade for Flipper:
Features.enabled?('unicorns', current_user)
In the frontend, feature flags are stored in Redux reducer and exposed via the following utilities:
// `LinkToUnicorns` would be shown, only when user have access to unicorns feature
<EnabledFeature name="unicorns">
<LinkToUnicorns />
</EnabledFeature>
// depending on user permission UnicornsPage or PageNotFound would be rendered
const UnicornsBranch = createFeatureFlaggedContainer({
featureFlag: 'unicorns',
enabledComponent: UnicornsPage,
disabledComponent: PageNotFound,
});
// ...
<Route path="/unicorns" component={UnicornsBranch} />
// ...
Sample implementation
Here is a sample Redux implementations of those components:
// This is quite simple reducer, containing only an array of features.
// You can attach this data to a `currentUser` or similar reducer.
// `BOOTSTAP` is global action, which contains the initial data for a page
// Features access usually don't change during user usage of a page
const BOOTSTAP = 'features/receive';
export default featuresReducer(state, { type, payload }) {
if (type === BOOTSTAP) {
return payload.features || [];
}
return state || [];
}
export function isFeatureEnabled(features, featureName) {
return features.indexOf(featureName) !== -1;
}
// This is your main reducer.js file
import { combineReducers } from 'redux';
export features, { isFeatureEnabled as isFeatureEnabledSelector } from './features';
// ...other reducers
export default combineReducers({
features,
// ...other reducers
});
// This is the important part, access to `features` reducer should only happens via this selector.
// Then you can always change where/how the features are stored.
export isFeatureEnabled({ features }, featureName) {
return isFeatureEnabledSelector(features, featureName);
}
Here are the components implementations:
import { connect } from 'react-redux';
import { isFeatureEnabled } from './reducers'
function EnabledFeature({ isEnabled, children }) {
if (isEnabled) {
return children;
}
return null;
}
export default connect((store, { name }) => { isEnabled: isFeatureEnabled(store, name) })(EnabledFeature);
import { isFeatureEnabled } from './reducers'
export default function createFeatureFlaggedContainer({ featureName, enabledComponent, disabledComponent }) {
function FeatureFlaggedContainer({ isEnabled, ...props }) {
const Component = isEnabled ? enabledComponent : disabledComponent;
if (Component) {
return <Component ..props />;
}
// `disabledComponent` is optional property
return null;
}
// Having `displayName` is very usefull for debuging.
FeatureFlaggedContainer.displayName = `FeatureFlaggedContainer(${ featureName })`;
return connect((store) => { isEnabled: isFeatureEnabled(store, featureName) })(FeatureFlaggedContainer);
}