Don't Surrender to Extraneous React Re-Renders

supremebeing7

Mark J. Lehman

Posted on November 7, 2019

Don't Surrender to Extraneous React Re-Renders

After learning about and monkeying around with this fantastic React tool why-did-you-render for about a week, I realized there was a lot that I didn't realize or understand about how React determines when to re-render a component. Here are 6 of the most helpful things I learned during this adventure.

1. Use React.memo for pure functional components

With React hooks, it's easier than ever to use functional components rather than class components. Larger and/or more complex components can be written as functions instead of classes. However, vanilla functional components re-render with every change to props, and when dealing with a large or complex component, that might not be necessary.

Enter React.memo. This makes a functional component behave similar to extending React.PureComponent -- namely, that it will do a shallow comparison of props on any prop change, and only re-render if previous props shallowly equal new props.

2. Pass in a comparison function for doing deep compares

Shallow comparison might not do the trick though. After all, maybe one of the props is an array of strings. If that array is generated on the fly somehow, for example by taking something from state and using map or filter to get only certain ones, even if the array contents hasn't changed, the new prop will be a new array, so prevProps.arrayProp === this.props.arrayProp will be false, and the component will re-render unnecessarily.

Luckily, React.memo takes a second argument that will be used to compare the props. So if there are limited props that can be compared deeply, that can avoid some unnecessary re-renders. A few notes about this:

  • The docs say this is not guaranteed to prevent re-renders. However, anecdotally I have noticed fewer re-renders using this approach.
  • Depending on how large or "heavy" the component is, and depending on how complex the props are, it's a good idea to determine whether it will be more performant to re-render or do a deep compare.
  • This is more or less analogous to the shouldComponentUpdate lifecycle method on React.Component, only in reverse (e.g. if shouldComponentUpdate returned true, the component would re-render; whereas if this passed-in function areEqual returns true, the component does not re-render.)

3. Only update state if it has changed

As you can see, the name of the game in reducing re-renders in general is to avoid props changes. Sometimes that will mean adding a bit more complexity elsewhere. For example, on our team, we like simple cases in reducers, such as this:

  case 'DOMAIN/UPDATE_ARRAY_PROP': {
    const { propName, arrayProp } = action;
    return Object.assign({}, state, {
      ...state, 
      [propName]: arrayProp
    })
  }

But, if state[propName] is deeply equal to arrayProp, we are reassigning that property even though it isn't actually changing. And as we just learned, reassigning the property, particularly when dealing with array and object props, creates a new array or object which will cause shallow comparisons to fail.

Instead, we should check if an UPDATE action is actually going to update, or if the updated values are the same as what's currently in state. If they are the same, don't update them and return state as-is to avoid the re-render. The above example, reworked (using lodash/isEqual):

  case 'DOMAIN/UPDATE_ARRAY_PROP': {
    const { propName, arrayProp } = action;
    // Add this guard!
    if (isEqual(state[propName], arrayProp)) return state;

    return Object.assign({}, state, {
      ...state, 
      [propName]: arrayProp
    })
  }

To further illustrate this, here's an example updating an object's property.

With extraneous re-renders:

  case 'DOMAIN/UPDATE_OBJECT_NAME': {
    const { objectName, newName } = action;

    return Object.assign({}, state, {
      ...state,
      [objectName]: {
        ...state[objectName], 
        name: newName
      }
    })
  }

Optimized:

  case 'DOMAIN/UPDATE_OBJECT_NAME': {
    const { objectName, newName } = action;
    // Add this guard!
    if (state[objectName].name === newName) return state;

    return Object.assign({}, state, {
      ...state,
      [objectName]: {
        ...state[objectName], 
        name: newName
      }
    })
  }

4. Avoid data conversion in selectors

Same problem, different symptom. When using selectors, avoid doing any data conversion if possible. This includes using map and filter. I have experience with selectors becoming a kind of repository of helper functions that do a lot of mapping and filtering. Using tools like reselect can help with this by memoizing the return values of the selectors.

Even so, some selectors might be better moved to helper functions, imported into the functions, and used to map or filter values pulled directly from state. Because a selector that pulls from state and then maps or filters will return a new array and re-render every time, whereas using a helper function in the component would have the component only re-render when that value in state has changed.

5. Get only what is needed from state

In selectors, fetch only what is needed in the component. For example, if I only want to check the count of some array of objects, I don't want to load the whole array into props, I just load the count for simpler shallow comparison.

6. No anonymous functions as props

I have seen and done this many times before realizing it was problematic:

<SomeComponent
  onError={() => console.error('BAD')}
/>

Every render of SomeComponent will compare that function against its previous iteration, and since it's anonymous, it will be effectively a different function each time, resulting in shallow prop comparison failure.

Instead, define functions outside the component and then pass in the named function:

const logError = () => console.error('BAD');
<SomeComponent
  onError={logError}
/>

There are also some more complicated and helpful examples in the issue tracker for why-did-you-render.

Conclusion

Remember that React itself seems generally very performant, so it's important to try not to get bogged down in wiping out all unnecessary re-renders. With small enough components, even if they re-render all the time, it likely won't have noticeable affects on app performance. For me, I choose to focus on the big heavy component re-renders and any low-hanging fruit for the smaller components, and I don't sweat the other stuff.

Image credit Louis Hansel
💖 💪 🙅 🚩
supremebeing7
Mark J. Lehman

Posted on November 7, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related