Go to main content
January 21, 2021
Cover image

Before getting to the heart of the matter, I want to precise it’s useless to focus on performances before getting real problems.

It is important to know the conditions that makes a component renders. There is 4 reasons:

  • its props have changed (props passed by the parent)
  • its state changed
  • the parent renders
  • a context used by the component has its value that has changed

Among these reasons, 2 force a potential useless re-render of the component (because no changes of props, state and/or value used from a React context). These conditions are the following ones:

Subsequently, we will focus to optimize when a children renders because of its parent.

Among all the lifecycle’s method that you know componentDidMount, componentDidUpdate, componentWillUnMount, … One allows to decide if a component needs to render or not. The name of this method is shouldComponentUpdate. It gets as parameters the next props and the next state. And it returns a boolean (true if the component has to render otherwise false).

shouldComponentUpdate(nextProps, nextState) {
    // Code
    // Returns a boolean if the component has to render or not
}

It’s possible to compare the current props this.props with the potential next ones nextProps, and compares this.state with nextState.

For example if we have a component:

  • which displays a “box” (which can be stylized)
  • gets a message as prop to display in that box

If the message does not change, the re-render of the component is not needed, we will have to compare the message before/after to decide to render or not:

import React from 'react';

export default class Box extends React.Component {
  shouldComponentUpdate(nextProps) {
    // If the message is not the same that the next one
    // then the component has to render
    // otherwise no
    return this.props.message !== nextProps.message;
  }

  render() {
    const { message } = this.props;

    return <div>{message}</div>;
  }
}

Potentially, we don’t want a component to render if the props and state do not change. This is exactly the use case of PureComponent.

A PureComponent is a Component that implements by default the lifecycle method shouldComponentUpdate. Its implementation is really simple, it compares with shallow equals the props before/after as well as the state. Basically it is:

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return (
      !shallowEqual(this.props, nextProps) ||
      shallowEqual(this.state, nextState)
    );
  }
}

We can reimplement our component Box with:

import React from 'react';

export default class Box extends React.PureComponent {
  render() {
    const { message } = this.props;

    return <div>{message}</div>;
  }
}

See codesandbox .

To distinguish a Component from a PureComponent, a property isPureReactComponent is added to the prototype of the function PureComponent visible here .

Then we will have the following steps:

  1. During the ReactFiberBeginWork phase, to know if the component needs to render, the method resumeMountClassInstance is called.
  2. This one will call the method checkShouldComponentUpdate
  3. If we are ine the case of a PureComponent then it returns the condition with shallow equals seen in the previous part.

Now, let’s explore how to code the same thing as previously but with functional components. You just have to use React.memo.

The signature is:

function MyComponent(props) {}

function arePropsEqual(prevProps, nextProps) {
  // Send true if the component hasn't to
  // render because the nextProps will produce
  // the same result than with prevProps
  // Otherwise send false
}

// The method arePropsEqual is optional
React.memo(MyComponent, arePropsEqual);

In the case of our component Box we get:

import React from 'react';

function Box({ message }) {
  return <div>{message}</div>;
}

export default React.memo(Box);

You can see this example in the codesandbox .

If we have a component, which takes as parameter a list, and needs to render only when the size of the list changes (I know that it’s a weird behavior but it’s just for the example):

import React from 'react';

function MyList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export default React.memo(
  MyList,
  (prevProps, nextProps) =>
    prevProps.items.length === nextProps.items.length,
);

We can notice that we have render problems, if we use PureComponent and React.memo (without a custom equal method), when we pass a children which is a component.

Thereafter, we will use functional components everywhere, but it’s possible to apply the first solution to implement the shouldComponentUpdate when working with component class.

With the Box we get:

import React from 'react';

function Box({ children }) {
  return <div>{children}</div>;
}

export default React.memo(Box);

And we use it this way:

import { useState } from 'react';
import MemoizedBox from './Box';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment me: {count}
      </button>
      <MemoizedBox>
        <h1>The tittle of the Box</h1>
      </MemoizedBox>
    </div>
  );
}

See the codesandbox .

We can see that the Box render everytime even though its children doesn’t seem to change (always the same title).

We can think that the solution is simple, we just have to make a component Title with our title which will be memoized.

import React from 'react';

function Title() {
  return <h1>The Title render</h1>;
}

export default React.memo(Title);

The usage will be:

import { useState } from 'react';
import MemoizedBox from './Box';
import MemoizedTitle from './Title';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment me: {count}
      </button>
      <MemoizedBox>
        <MemoizedTitle />
      </MemoizedBox>
    </div>
  );
}

When we test again, we realize that it doesn’t work either. While the component Title do not re-render, the Box renders.

So why does the component render even though the children doesn’t?

To understand the problem, it’s important to know how works the transpilation of the code (with babel for example).

Actually our Title component, after transpilation, is transformed to the following code:

var React = require('react');

function Title() {
  return React.createElement(
    'h1',
    null,
    'The Title render',
  );
}

Now we have to understand what does the method React.createElement return. This function is visible on github here .

We can see that it executes the method ReactElement and returns the result. The code of the method ReactElement is here .

ReactElement returns an object which have the next structure visible here :

const element = {
  // This allows us to identify this as a ReactElement
  // The constant is a symbol
  $$typeof: REACT_ELEMENT_TYPE,

  // Type of the element
  // For the title it's a h1
  type: type,
  // The potential key passed to the component
  key: key,
  // The potential reference of the component (prop ref)
  ref: ref,
  // Contains all the props, the children
  // is merged to this
  props: props,

  // The component which has created the element
  // Type: Fiber
  _owner: owner,
};

We can imagine we have to use deep equality and not just shallow equality.

Let’s test this quickly by using the deep equal of lodash . Link to the codesandbox .

It doesn’t solve the render problem. It comes from the _owner property of our ReactElement which contains circular dependencies.

The particularity of the library react-fast-compare is that it does process the comparison of the key _owner: see here .

import reactFastCompare from 'react-fast-compare';

function Box({ children }) {
  console.log('The Box render');
  return <div>{children}</div>;
}

return React.memo(Box, reactFastCompare);

Used like this:

import { useState } from 'react';
import MemoizedBox from './Box';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment me: {count}
      </button>
      <MemoizedBox>
        <h1>The Title render</h1>
      </MemoizedBox>
    </div>
  );
}

With the help of the library, there is no more re-render of the Box and we do not need to make a component Title: see codesandbox .

The second solution is to use useMemo to memoize the title and pass it as children to the Box component:

import { useState, useMemo } from 'react';
import MemoizedBox from './Box';

export default function App() {
  const [count, setCount] = useState(0);

  const renderTitle = useMemo(
    () => <h1>The Title render</h1>,
    [],
  );

  return (
    <div>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment me: {count}
      </button>
      <MemoizedBox>{renderTitle}</MemoizedBox>
    </div>
  );
}

And the Box code:

import React from 'react';

function Box({ children }) {
  return <div>{children}</div>;
}

export default React.memo(Box);

You can test this here .

We have just seen the solutions not to have useless re-render. But it is important not to have changing references when it is not needed, because most of the time we will use shallow equal for performances (no need to deeply compared objects).

In this case defines classes methods, do not define them in the render method.

import React from 'react';
import MemoizedComponent from './MemoizedComponent';

class MyComponent extends React.Component {
  myMethod = () => {};

  render() {
    return <MemoizedComponent myMethod={myMethod} />;
  }
}

If you need to process data to pass an object to the component, make a memoized method not to change the reference of the output if the input parameters do not change. You don’t need of memoization if it returns a primitive.

import React from 'react';
import MemoizedComponent from './MemoizedComponent';
import memoize from 'memoize-one';

class MyComponent extends React.Component {
  getMyData = memoize((inputParameters) => {
    // Process data and return the output
    return output;
  });

  render() {
    return (
      <MemoizedComponent
        myData={this.getMyData(potentialParameters)}
      />
    );
  }
}

If you want to know how works memoization in javascript you can read this article .

If you need to use a method in a component or to pass it to a component, it can be useful to take the method out of the component if it does not need data of the component (props or state). This way the method will have a static reference.

For example, go from:

import MemoizedInputField from './InputField';

// An example of formular with a field which is valid uniquely
// when the value is Rainbow
function MyForm() {
  // The validation method is declared inside the component (in the render)
  // It does not use any data of the component MyForm
  // Its reference is changing each time the component renders
  // which makes the InputField renders even if memoized
  const validate = (name) => name === 'Rainbow';
  return (
    <MemoizedInputField label="name" validate={validate} />
  );
}

To:

import MemoizedInputField from './InputField';

// The method validate has been extracted
// Its reference does not change anaymore
const validate = (name) => name === 'Rainbow';

function MyForm() {
  return (
    <MemoizedInputField label="name" validate={validate} />
  );
}

In this case, if InputField is a memoized component, it will not render each time than MyForm renders, contrary to the first snippet.

When you have to pass a callback which needs data from the current component, it can be useful to use the hook useCallback to keep a fixed reference as soon as the data does not change.

import MemoizedButton from './Button';

function IncrementButton({ setCount }) {
  // Each time the component renders
  // a new reference is created
  const incrementCount = () =>
    setCount((prevCount) => prevCount + 1);

  return (
    <MemoizedButton onClick={incrementCount}>
      Increment count
    </MemoizedButton>
  );
}

Each time IncrementButton render, the component Button will render too even if the component is memoized, because the callback incrementCount is recreated.

The solution is to use useCallback:

import { useCallback } from 'react';
import MemoizedButton from './Button';

function IncrementButton({ setCount }) {
  // The callback is only recreated when setCount changes
  const incrementCount = useCallback(
    () => setCount((prevCount) => prevCount + 1),
    [setCount],
  );

  return (
    <MemoizedButton onClick={incrementCount}>
      Increment count
    </MemoizedButton>
  );
}

It can be also useful to use the hook useMemo , while we pass objects calculated in the render. This will stabilize the reference as soon as the dependencies do not change.

import MemoizedTable from './Table';

const COLUMNS = [
  {
    name: 'productName',
    label: 'Product name',
  },
  {
    name: 'productCategory',
    label: 'Category',
  },
];

function PageTable({ products }) {
  // A new reference is created
  // each time the component renders
  // event if the products do not change
  const data = products.map((product) => ({
    productName: product.name,
    productCategory: product.category,
  }));

  return <MemoizedTable columns={COLUMNS} data={data} />;
}

We can transform to:

import { useMemo } from 'react';
import MemoizedTable from './Table';

const COLUMNS = [
  {
    name: 'productName',
    label: 'Product name',
  },
  {
    name: 'productCategory',
    label: 'Category',
  },
];

function PageTable({ products }) {
  // We calculate the data only when the products change
  const data = useMemo(
    () =>
      products.map((product) => ({
        productName: product.name,
        productCategory: product.category,
      })),
    [products],
  );

  return <MemoizedTable columns={COLUMNS} data={data} />;
}

You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.