React and rerendering

A functional component will refresh any time it receives new props, more precisely if its old and new props fail a referential identity check.

Props must be treated as immutable. Just like function arguments, you should never modify props from within the functional component. Doing so will not trigger a refresh.

Consider the following component:

const App = () => {
  const [message, setMessage] = React.useState('');
  return (
    <>
      <Tile message={message} />
      <Tile />
    </>
  );
};

When the state changes in the parent component (in this case, App), the two Tile components will re-render, even though the second one doesn’t even receive any props.

This means that the render function is being called 3 times, but actual DOM modifications only happen once in the Tile component that displays the message.

React schedules a render every time the state of a component changes with useState or setState. Not only does that mean that the render function of the component will be called, but also that all its subsequent child components will re-render, regardless of whether their props have changed or not.

shouldComponentUpdate(nextProps, nextState) {
  // Returning true causes React to call the render function, returning false prevents this
}

If you have a global state defined with useContext and the global state changes, then it triggers recalculations or re-rendering of all the visible React components.

React context uses reference identity to determine when to re-render. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value

class App extends React.Component {
  render() {
    return (
      <Provider value={{key: 'value'}}>
        <Toolbar />
      </Provider>
    );
  }
}

Another example

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []} />
         )}
       </div>
     );
  }
}

In this example this.props.options || [] when the options prop is null then the default arrays is going to be used. As you should know the array literal is the same as new Array()which creates a new array instance. This completely destroyed every pure render optimization inside the Cell elements. In Javascript different instances have different identities and thus the shallow equality check always produces false and tells React to re-render the components.

The way to fix this would be:

const default = [];
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || default} />
         )}
       </div>
     );
  }
}

Memo derives from memoization. It means that the result of the function wrapped in React.memo is saved in memory and returns the cached result if it’s being called with the same input again.

const Tile = React.memo(() => {  let eventUpdates = React.useRef(0);
  return (
    <div className="black-tile">
      <Updates updates={eventUpdates.current++} />
    </div>
  );
});

Let’s say we also pass an object property down to the Tile component

const App = () => {
  const updates = React.useRef(0);
  const [text, setText] = React.useState('');
  const data = { test: 'data' };
  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo data={data} />      </div>
    </div>
  );
};

Suddenly, React.memo seems to stop working and the memoized component renders on every keystroke again. The reason React.memo doesn’t work anymore is because it only does a shallow comparison of the component’s properties. The data variable is being re-declared on every update of App. This leads to the objects not actually being the same because they have different references.

React.memo provides a solution for this in its second parameter. This parameter accepts a second areEqual function, which we can use to control when the component should update.

const TileMemo = React.memo(() => {
  let updates = React.useRef(0);
  return (
    <div className="black-tile">
      <Updates updates={updates.current++} />
    </div>
  );
}, (prevProps, nextProps) => {  if (prevProps.data.test === nextProps.data.test) {    return true; // props are equal  }  return false; // props are not equal -> update the component});

Alternatively you can wrap the object in React.useMemo()

const data = React.useMemo(() => ({
  test: 'data',
}), []);

The second parameter of useMemo is an array with the dependencies of the variable. If one of them changes, React will recompute the value.

Dealing with functions

In JavaScript, functions behave just like objects, which leads to the same problem we had before. The onClick function is being declared each time App updates. TileMemo then thinks that onClick changed because the reference has changed.

const App = () => {
  const updates = React.useRef(0);
  const [text, setText] = React.useState('');
  const onClick = () => {    console.log('click');  };
  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo onClick={onClick} />      </div>
    </div>
  );
};

We can memoize the function with useCallback:

const onClick = React.useCallback(() => {
  console.log('click');
}, []);

Why not use React.memo by default?

You could think that React.memo doesn’t have any downsides and that you should wrap all your function components. The problem with that is that React.memo explicitly caches the function, which means that it stores the result (VDOM) in memory. If you do this with too many or too big components this leads to more memory consumption. That’s why you should be careful when memoizing large components. The other case where you should avoid using it is when the component’s props change frequently. React.memo introduces additional overhead which compares the props with the memoized ones. This might not affect performance too much but you certainly miss out on the performance optimization React.memo is made for.

React is already very efficient at optimizing rendering performance. Most of the time you shouldn’t bother spending time on optimizing unnecessary re-renders. The first step should always be to measure and identify performance bottlenecks. It’s a good idea to profile your React app beforehand to see which components render the most. Applying React.memo to those components will have the biggest impact.

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.