It's too simple to write slow react code

In the early years, react was marketed with and and well-renowned for its rendering performance.

Nowadays, most core concepts have been adapted by the competition, and react is no longer the absolute forefront, especially if the boot up time is considered. The combination of react and react-dom is heavy.

Delivering fast web applications with react is totally possible, but here’s the but: Doing so requires considerable knowledge on how react works internally, and constant vigilance.

A month back for instance, I pushed a small feature-addition to an existing component. The code looked like this:

There was a container component index.js:

import { connect } from 'react-redux';

import { selectSomeObjectFromState } from 'data/selectors';
/* ...a few more reselect selectors... */

import MyComponent from './MyComponent';

const mapStateToProps = (state) => ({
    myArray: selectSomeObjectFromState(state).myArray || [],
    /* ...more props selected from redux state... */
});

export default connect(mapStateToProps)(MyComponent);

And then the actual component MyComponent.js:

import React, { memo } from 'react';
import PropTypes from 'prop-types';

import styles from './MyComponent.css';

const MyComponent = ({ myArray, /* ...those other props... */ }) => (
    <div className={styles.MyComponent}>
    {
        /* render some stuff */
    }
    </div>
);

MyComponent.propTypes = {
    myArray: PropTypes.array.isRequired,
    /* the other props */
};

export default memo(MyComponent);

Let’s recap what we saw:

  1. First, there was the container component that consumes some object from the redux state, and passes an attribute to the component. If the value is set falsy in the redux state, an empty array is passed.
  2. The component receives quite a few props, but the rendering is so straightforward that a functional component was the obvious choice. I knew that the component re-renders a couple of times, which is why I wrapped its export with React.memo.

Can you spot the mistake?

Of course 💡: Every time that prop myArray is falsy in the redux state, a new array is passed to the component. The shallow equality check of memo detects a change. The result is that MyComponent re-renders even when there is no change in props. I was considerate enough to try to prevent unnecessary re-rendering with memo, and accidentally caused the opposite.

Looking at this code in isolation it’s easy to spot the error, and easy to fix:

  • I could write MyComponent in a way that it could deal with myArray being passed with falsy values.
  • I could use propTypes with a default prop definition, which for myArray would define an empty array. propTypes does not instantiate a new array for every render call.
  • I could change the reselect selector. Selectors are memoized by default. The selector could return an empty array instead of a falsy return value.

But maybe, you share my take:

A UI library should nudge its users into the direction of writing performant code. It should warn me, when I’m sloppy. And, ideally, slow code would be just a tad less subtle.