Runtime-check the re-renderings

Runtime-check the re-renderings

By Guillaume Claret

To optimize the rendering of React components, we often use the PureRenderMixin. The PureRenderMixin mixin only re-renders the components when the props or the state changed according to the shallow equality. This suppose that:

  • we use immutable objects (so that the shallow equality always implies the deep equality);
  • we rarely create fresh but identical objects (so that the deep equality often implies the shallow equality).

In order to enforce this workflow, we propose some runtime checks to assert that the shallow equality is indeed equivalent to the deep equality over the props. This helped us to remove some useless re-renderings, and protects us against mutation bugs.

Run-time checks

The standard definition of the shouldComponentUpdate method in pure components is the following:

shouldComponentUpdate(nextProps, nextState) {
    return !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState);
  }

where shallowEqual is the shallow equality over an object.

No mutations

We assert that the shallow equality must imply the deep equality. We add the following code in the shouldComponentUpdate body. This relies on the isEqual function from lodash.

const arePropsShallowEq = shallowEqual(this.props, nextProps);
const areStatesShallowEq = shallowEqual(this.state, nextState);
const arePropsEq = _.isEqual(this.props, nextProps);
const areStatesEq = _.isEqual(this.state, nextState);

const {displayName} = this.constructor;

// Check that we do not mutate.
if (arePropsShallowEq && ! arePropsEq) {
  console.error(`In ${displayName} the props were mutated from`, this.props, 'to', nextProps);
}
if (areStatesShallowEq && ! areStatesEq) {
  console.error(`In ${displayName} the state was mutated from`, this.state, 'to', nextState);
}

No useless re-renderings

We assert that the deep equality should imply the shallow equality:

// Check that we do not re-render for nothing.
if (! arePropsShallowEq && arePropsEq) {
  console.warn(`In ${displayName} the props did not change but are not ` +
    'equal according to `===`', this.props);
}
if (! areStatesShallowEq && areStatesEq) {
  console.warn(`In ${displayName} the state did not change but is not ` +
    'equal according to `===`', this.state);
}

No fresh functions

One last thing, we check that we do not recreate fresh functions for the event handlers in the props. We consider it as a bug since we view it as a performance anti-pattern (see React.js pure render performance anti-pattern):

// We create a fresh `onChange` prop, thus triggering a useless rendering.
<MyPureComponent onChange={this.onChange.bind(this)} />
// Same problem.
<MyPureComponent onChange={event => this.onChange(event)} />

The code to assert that we do not recreate functions is the following:

// Check that we do not create new functions.
if (this.props !== null && nextProps !== null) {
  for (const key in nextProps) {
    if (typeof nextProps[key] === 'function' && key in this.props &&
      typeof this.props[key] === 'function' && this.props[key] !== nextProps[key]) {
      console.error(`In ${displayName} the function ${key} changed in the props`);
    }
  }
}

Of course, we should not run these checks in production. In our setting with webpack, we wrap these checks into these lines:

if (process.env.NODE_ENV !== 'production') {
  ... // checks
}

Related

On a related note, we also recommend to activate the eslint-plugin-immutable plugin to syntaxically forbid standard sources of mutations in JavaScript. This plugin would forbid the following lines:

let n = 12; // forbidden (let)
o.address = 'here'; // forbidden (field assignment)
OuiCar's Picture

About OuiCar

OuiCar is a community market place to find and rent cars anywhere in France. But we are not just car renters. We also like to experiment new technologies and share ideas with others.

Paris http://www.ouicar.fr

Comments