react

A 4-post collection

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);
}

(code in gist)

Handling paths in React application

When using React Router, "a" components should be replaced by Link:

<Link to="/about">About</Link>  
<Link to={`/${post.categorySlug}/${post.slug}`}>{post.name}</Link>  

Passing a route to Link as string works, but doesn't protect us from mistyping. Also changing routes is not very easy. For example if we decide that /${post.categorySlug}/${post.slug} should become /posts/${post.slug}, there would be a lot of grepping.

Ruby on Rails solves those problems by generating a method for every route your application.

This concept works great with React Router:

<Link to={paths.about()}>About</Link>  
<Link to={paths.post(post)}>{post.name}</Link>  

All you have to do is define all your routes in file:

// routes.js
export default {  
   post(post) {
     return `/${post.categorySlug}/${post.slug}`;
   },

   about() {
    return '/about';
   },

   // not all routes are strings
   contacts() {
     return { pathname: 'contacts', state: { modal: true } }; 
   },

   // helper for image sources
   image(path) {
     return `https://product-hunt-cdn.com/images/${path}`
   }

   // ....
};

This technique works great with flow, giving you type safety in your links.

I have thought several times about generating this file from router component. But didn't have a chance to do so and with React Router v4 this won't be very easy.

Flow as replacement for PropType

A couple of months ago at Product Hunt, we decided to switch from React.PropTypes to Flow. Initially, we started using Flow just for function definitions. Then we start replacing propType.

Why we do did that? Let's say we have an UserImage component with two possible uses:

<UserImage user={user} width={50} height={30} />  
<UserImage user={user} variant="small" />  

Its definition would be something like this:

const UserShape = {  
  id: React.PropTypes.number.isRequired,
  name: React.PropTypes.name.isRequired,
};

class UserImage extends React.Component {  
  static propTypes = {
    user: React.PropTypes.shape(UserShape).isRequired,
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    variant: React.PropTypes.oneOf(['big', 'medium', 'small')
  };

  render() { /* ... */ }
}

There are some problems with that definition:

  • UserShape can be used only with other React components
  • propTypes throws a warning during runtime
  • width and height should be passed together, but we can't enforce that
  • It is possible to pass all three props - width, height, variant

Here is how the Flow solves those problems:

type User = {  
  id: number,
  name: string,
};

class UserImage extends React.Component {  
  props: {
    user: User,
    width: number,
    height: number,
  } | {
    user: User,
    variant: 'big' | 'medium' | 'small',
  };

  render() { /* ... */ }
}
  • User is a generic type (can be used for any javascript function)
  • Flow types are used only during build
  • <UserImage user={user} width="10" /> breaks the build 😎

Unfortunately this is still possible:

<UserImage user={user} width={50} height={30} variant="small" />  

One way to solve that is the following:

  props: {
    user: User,
    width: number,
    height: number,
    // expects "variant" is not passed
    variant?: void,
  } | {
    user: User,
    variant: 'big' | 'medium' | 'small',
    // expects "width" and "height" are not passed
    width?: void,
    height?: void,
  };

It's a bit ugly, but it gets the job done:

<UserImage user={user} width={50} height={30} variant="small" />  
                       ^^^^^^^ number. This type is incompatible with void

<UserImage user={user} width={50} height={30} variant="small" />  
                                   ^^^^^^^ number. This type is incompatible with void

<UserImage user={user} width={50} height={30} variant="small" />  
                                                       ^^^^^^^ string. This type is incompatible with void

For more information check Flow documentation.

p.s. Gabriele Petronella and Vjeux told me on twitter about $Exact and {| |}:

  props: 
    {| user: User, width: number, height: number |} |
    {| user: User, variant: 'big' | 'medium' | 'small' |};

Using "data-test" in testing

When testing HTML components, I often see people using class names as selectors. For example:

element.find('.description button.expand-button').simulate('click');  

While this seems convenient at first, there are some drawbacks. HTML structure and css classes tend to change due to design changes. Which will cause you re-write tests quite often. Also, if you are using css-modules you can't rely on class names.

Because of that, for quite some time now, I have started marking elements with data-test attribute.

React example (using enzyme and chai-enzyme):

describe(Description.name, () => {  
  it('cut off text based on `cutoffLength`', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);

    expect(el).to.have.text('t...');
    expect(el).not.to.have.text('test');
  });

  it('hides expand button when text is short', () => {
    const el = shallow(<Description text="test" cutoffLength={10} />);
    expect(el).not.to.have.descendants('[data-text="expand-button"]');
  });

  it('shows expand button when text is long', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);
    expect(el).to.have.descendants('[data-test="expand-button"]');
  });

  it('clicking expand button reveals the whole text', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);

    el.find('[data-test="expand-button"]').simulate('click');

    expect(el).not.to.have.descendants('[data-test="expand-button"]');
    expect(el).to.have.text('test');
  });
});

The component code:

import React from 'react';  
import styles from "./style.css";

export default Description extends React.Component {  
  state = { expanded: false };

  render() {
    const { text, cutoffLength } = this.props;

    if (this.state.expanded || text.length < cutoffLength) {
      return (
        <div className={styles.description}>
          {this.props.text}
        </div>
      );
    }

    return (
      <div className={styles.description}>
        {`${ text.substr(0, cutoffLength) }...`}
        <button 
          data-test="expand-button" 
          className={styles.expand} 
          onClick={this.expand}>show more</button>
      </div>
    );
  }

  expand = () => {
    this.setState({ expanded: true });
  };
}

I'm also using data-test attributes for testing with Capybara in Ruby land.

describe 'Product page' do  
  it 'has product description rev' do
    product = create :post, :with_long_description

    visit product_path(product)

    expect(page).not_to have_text product.description

    # This can be extracted into `find_test_attr` or `click_test_attr`
    find(:css, '[data-test="expand"]').click

    expect(page).to have_text product.description

    # This can be extracted into `have_test_arr`
    expect(page).not_to have_css('[data-test="expand"]')
  end
end