Skip to content

An article on how to manage React renderings using React.PureComponent or React.memo

License

Notifications You must be signed in to change notification settings

mbeaudru/react-performances-rendering-management

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 

Repository files navigation

React performances: how to manage re-renderings

Goal

When you are choosing React as your primary library for your application, you probably will want to know how to write efficient components to provide a good experience to your users.

There are many ways to improve your apps performances (images compressions, memoization, code splitting to name a few) but here we will try to focus on how to write your components so that you can take advantage of React and not the opposite.

You might have heard about React.PureComponent or React.memo which are indeed the tools you will want to use to improve some parts of your React app.

But wrongly used, you might end up having a hard to maintain / buggy codebase. We will see how and when to use those tools in a minute!

When does a component re-renders ?

By default, a component will re-render if:

  • One of the props the component receives have changed
  • A component state variable has been updated
  • It listens to a context variable that changes
  • The update is forced by the developer using this.forceUpdate (class syntax)
  • Each time the parent that calls the component is being rendered

We will see that the first four points are quite straightforward, but hang on because the last one is the reason I'm writing this article!

Practical example:

Let's figure out why Button re-renders when one of the above event happens:

Note : I'm omitting the forceUpdate and context cases in this example to simplify the reasoning, but if you want to know more about them just read the forceContext API docs and React Context docs.

Button.js

import React from "react";

class Button extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      active: false
    };
  }

  toggleActiveState = () => {
    this.setState(prevState => ({ active: !prevState.active }));
  };

  render() {
    const { color } = this.props;
    const { active } = this.state;
    return (
      <button
        style={{ backgroundColor: color }}
        onClick={this.toggleActiveState}
      >
        {active ? "I am so actived right now!" : "Not very active today..."}
      </button>
    );
  }
}

export default Button;
  • props changes

Our Button takes a color prop which is used to determine the color of the button on screen. Whenever this property changes, we want the component to be re-rendered so that React reflects that change by updating the DOM for us.

  • state changes

Our Button has an inner state active which is a boolean used to determine whether we should print I am so actived right now! or Not very active today... on screen. Again, whenever this property evolves React must update the DOM to reflect the changes, that's why our Button is re-rendered.

Parent re-renderings makes all children re-render

At this point, you might be wondering:

You, wondering : If the props, the inner state and the context of the component has not changed, the component doesn't need to be re-rendered right ?

Yup, totally ! And yet, even if your props and state haven't changed, React will make your components re-render each time the parent that calls your components is being rendered.

ButtonGroup example

To illustrate what a parent re-render means, let's use a that print our previous multiple times. This component will have a state variable counter that will be displayed on screen.

import React from "react";
import Button from "./Button";

class ButtonGroup extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    };
  }

  incrementCounter = () => {
    this.setState(prevState => ({ counter: prevState.counter + 1 }));
  };

  render() {
    const { counter } = this.state;

    return (
      <div>
        <h1 onClick={this.incrementCounter}>Counter value: {counter}</h1>

        <Button color="red" />

        <Button color="blue" />

        <Button color="yellow" />
      </div>
    );
  }
}

export default ButtonGroup;

When you click on the <h1> title, the state variable counter of will evolve and trigger a re-render of the component . But not only ! All our <Button /> components will be re-rendered as well, even if their props haven't evolved and their inner state remains the same.

Sounds absurd and inefficient ? Well, let's dig in to understand why they made that choice and how you should deal with that in your code!

Props and state diffing

To say that a prop or state variable has changed is more complex that it seems depending on the kind of variable we are dealing with.

Variables

Primitive variables

List of primitive variables (ref: MDN):

  • Boolean
  • null
  • undefined
  • Number
  • String
  • Symbol (new in ECMAScript 6)

If we are dealing with a primitive variable, it's quite easy to check if there's been a change because you can compare their values directly:

const foo = 1;
const bar = 1;

console.log(foo === bar); // true

"Object" variables

In JavaScript, object variables are for instance:

  • Functions () => {}
  • Array []
  • Literal objects {}

MDN: In computer science, an object is a value in memory which is possibly referenced by an identifier.

As stressed out, the key word in the sentence in referenced. Indeed, when you compare two "object" variables you are not comparing their values but their in-memory references.

const foo = {}; // creates an object with a reference X in memory
const bar = {}; // creates another object with another reference Y in memory

console.log(foo === bar); // false ! Despite having the same "values", the "===" operator only compares the in-memory references which are different (X and Y respectively)

React perspective

That being said, let's get back into our initial issue: why does React decides to re-render all the children components when the parent is being re-rendered ?

Object comparisons

When it comes to rendering optimizations, React gives you two solutions:

  • shouldComponentUpdate / React.Memo custom handler

Using shouldComponentUpdate lifecycle we can take the control and override the default React "re-rendering" behavior.

Every time React is tempted to re-render (because of one of the events above) it will run this method. The return value of shouldComponentUpdate is a boolean, and React will re-render our component only if the return value is true.

  • Use React.PureComponent (or React.Memo for functional components)

Implements a shouldComponentUpdate for you using a shallow comparison algorithm.

Speed vs precision trade-off

As we've just seen, when we compare objects we in fact compare their references and not their values. Let's see what happens when you shallow compare props using a PureComponent

ParentComponent.js

class ParentComponent extends React.Component {
  componentDidMount() {
    setTimeout(() => this.forceUpdate(), 500);
  }

  render() {
    const foo = {
      bar: "baz"
    };

    return <ChildComponent foo={foo} />;
  }
}

ChildComponent.js

class ChildComponent extends React.Component {
  shouldComponentUpdate(nextProps) {
    // This is how PureComponent works, but it loops on your props in addition
    console.log(
      nextProps.foo, // { bar: "baz" }
      this.props.foo, // { bar: "baz" }
      nextProps.foo === this.props.foo // false ! Despite having the same values, the references are different
    );

    // Will return "true" despite we know the result in the DOM will be the same...
    return nextProps.foo !== this.props.foo;
  }

  render() {
    return <div>{this.props.foo.bar}</div>;
  }
}

In trying to improve the performances of our app in saving a render, we actually made it worse ! Indeed, we just made a useless foo prop diffing that still triggered a re-render.

Besides, if the ParentComponent was doing weird things with the object it is passing to ChildComponent (like make mutations to the object), a shallow comparison could even make the component not to render while it should have, breaking your UI!

In conclusion, shallow comparison algorithm is a nice tool to use where you are really needing it but it should not be generalized.

Note : Unless you use a library like ImmutableJS to manage all your Objects but that's another story !

So, can't we make a more precise check to solve this problem ? Well... yeah:

ChildComponent.js

class ChildComponent extends React.Component {
  shouldComponentUpdate(nextProps) {
    console.log(
      nextProps.foo.bar, // "baz"
      this.props.foo.bar, // "baz"
      nextProps.foo.bar === this.props.foo.bar // true
    );

    // It returns false, that worked
    return nextProps.foo.bar !== this.props.foo.bar;
  }

  render() {
    return <div>{this.props.foo.bar}</div>;
  }
}

But this won't scale, because you need to dig in for each leaf of your objects until you reach a primary type you can value-compare.

Indeed, your objects will probably have more keys / values than this and they might be deeper too ! In order to work, every time your prop shape changes you would need to go back to this shouldComponentUpdate function and make sure you didn't forgot any keys. That's a huge source of bugs and maintenance issues.

There are some libraries and techniques to deep compare two objects, but it comes at a cost that will totally defeat the initial purpose unless you use them very cautiously.

To sum it up

You don't want to use React.PureComponent / React.memo everywhere because:

  • It might harm performances if you create "Object" variables in the render and pass them by props, because it would always make the component re-render while making you pay the cost of state and props comparison
  • It might create bugs, if a prop variable is mutated in the parent the component might not re-render because it only shallow compares, which would break your app

And you don't want to do deep "Object" comparisons either because it is quite costly.

You might want to use React.PureComponent / React.memo sparingly:

  • When you have measured that useless and costly re-renders could be optimized
  • If you have control on how your component is used by the parent, no useless reference changes in the "Object" props nor mutations

Conclusions

Now that we've seen that performance optimizations couldn't be safely generalized and used with caution, here is my suggestion on how to manage react renderings in an application:

  • First, write the most clear and maintainable code you can.
  • If you have a doubt on whether the re-renders are costly or not, measure it using the devTools before trying to optimize your code.
  • If you find out that some components are rendering too often and are costly, use React.PureComponent / React.memo or implement your own shouldComponentUpdate as a last resort.

If you liked this content

You can encourage me to do more in appending a "Star" to this repo or following me on Twitter @mbeaudru !

About

An article on how to manage React renderings using React.PureComponent or React.memo

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published