Optimizing computations and re-renders in React

In React, components are used to encapsulate and organize UI elements as independent sections of re-usable code. When a component is initally rendered, React will create a Virtual DOM representation of the UI and check when the state or props of a component has been modified to compare and reflect the changes onto the real DOM.

While this does a great job at minimizing the total number of direct DOM manipulations, (therefore increasing performance), unnecessary re-renders and re-computations can still be a potential performance bottle-neck if you’re not careful.

Let’s have a look at an example of how and why this might occur, solutions using memoization and Callbacks and how this will be handled for you in React 19.

1. Parent and Child Components

import './App.css';
import { useState } from 'react';
import ChildComponent from './ChildComponent';

function App() {

  const [count, setCount] = useState(0);
  const [listOfUsers, setListOfUsers] = useState(["Bob", "Terry"]);

  const increment = () => {
    setCount((i) => i + 1);
  };

  return (
    <div className='App'>
      <ChildComponent props={listOfUsers} />
      <div>
        Unrelated Number: {count}
        <br/>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default App;

In React, components can take JavaScript values as parameters to display or apply some logic to them. These are called props.

Here we have our main App component (in a file named App.js) which simply renders two things:

However, if we use the button to increment the count, the ChildComponent will re-render every time even though it’s props nor state has changed.

This is because when a parent component updates, all ‘child components’ (Components nested within other components) will re-render too. In this case, whenever the button is pressed, the state for count in the App component is updated which causes the ChildComponent to re-render even though the child component itself has not changed.

Memoization is the technique used to optimize the performance of functions by caching the results of (expensive) function calls and returning the cached result when the same inputs occur again. We will solve this problem by using React’s in-built memo function.

By wrapping Reacts memo function around the ChildComponent, the component will be memoized and we can skip re-rendering a component if it’s props remain unchanged. In this case listOfUsers, the prop passed to the ChildComponent, does not change when the App’s state changes, hence it does not trigger subsequent re-renders.

Before:

const ChildComponent = ({ props}) => {
    console.log("List of users rendered");
    return (
      <>
     {props.map((user, index) => {
        return <p key={index}>{user}</p>;
      })}
      </>
    );
  };
  
  export default ChildComponent;

After:

import { memo } from "react";

const ChildComponent = ({ props}) => {
    console.log("List of users rendered");
    return (
      <>
     {props.map((user, index) => {
        return <p key={index}>{user}</p>;
      })}
      </>
    );
  };
  
  export default memo(ChildComponent);

After implementing this, the ChildComponent no longer re-renders on every button click.

2. Referential Equality and CallBacks

We can also pass functions as a prop for components to use. These functions are known as Callback functions.

Going back to our previous example, say we want the ChildComponent to not only display a list of ‘users’ but also have a button that adds a new user. We can do this by passing the function addUser, which appends the current list with a new user, as a prop for the ChildComponent to call upon when the button is pressed. For example:

App.js:

import './App.css';
import { useState } from 'react';
import ChildComponent from './ChildComponent';

function App() {

  const [count, setCount] = useState(0);
  const [listOfUsers, setListOfUsers] = useState(["Bob", "Terry"]);

  const increment = () => {
    setCount((i) => i + 1);
  };

  // New add user function
  const addUser = () => {
    setListOfUsers((prev) => [...prev, "Anonymous"]);
  };

  return (
    <div className='App'>
      <ChildComponent listOfUsers={listOfUsers} addUser={addUser}/>
      <div>
        Number count: {count}
        <br/>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default App;

ChildComponent.js

import { memo } from "react";

const ChildComponent = ({ listOfUsers, addUser }) => {
    console.log("List of users rendered");
    return (
      <>
     {listOfUsers.map((user, index) => {
        return <p key={index}>{user}</p>;
      })}

      // New button to add users
      <button onClick={addUser}>Add User</button>
      </>
    );
  };
  
  export default memo(ChildComponent);

ChildComponent re-renders when a new user is added every time the AddUser button is pressed, as expected. However, it now has the same problem we encountered before where ChildComponent is re-rendered everytime the increase count button is presssed. With memo implemented, it should not be re-rendered if props and state remains unchanged which seems to be the case so whats happening?

The answer is that the props did change. When a component re-renders, their functions are also reconstructed. When the App component re-renders, a new instance of the addUser function is considered as a new prop being passed to the ChildComponent. This is because React checks if the props are the same using referential equality. This means checking if two objects reference the same memory location to determine if they are equal rather than comparing their actual value (this is due to functions being a non-primative data type whereas a primative data type would be compared by value instead).

A solution to this is to use the React useCallBack hook. This hook memoizes a given function and returns it on subsequent re-renders where the dependencies has not changed. In other words, the function will not be re-created if none of its dependencies change. In our example we’ll make listOfUsers a dependency:

import './App.css';
import { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';

function App() {

  const [count, setCount] = useState(0);
  const [listOfUsers, setListOfUsers] = useState(["Bob", "Terry"]);

  const increment = () => {
    setCount((i) => i + 1);
  };

  const addUser = useCallback(() => {
    setListOfUsers((prev) => [...prev, "Anonymous"]);
  }, [listOfUsers]);


  return (
    <div className='App'>
      <ChildComponent listOfUsers={listOfUsers} addUser={addUser}/>
      <div>
        Number count: {count}
        <br/>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default App;

Since listOfUsers, the only dependency for the addUser function, does not change when the increment button is called, the function is not re-constructed, no new prop is passed to the ChildComponent and therefore, unnecessary re-rendering of the component is avoided.

3. Repeating the same computation

Say you’re in a scenario where you need to re-render a component but the component also performs an expensive computation whenever it is rendered. If the input remains the same, you can also use memoization to cache the result to avoid re-computations (but if input is mostly changing, then caching is redundant). useMemo is a React hook that can memoize the result of a pure function i.e. has no side effects. For example:

Before:

import './App.css';
import { useState, useCallback} from 'react';
import ChildComponent from './ChildComponent';

const someExpensiveComputation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 500000000; i++) {
    num *= 2 % 5;
  }
  return num;
};

function App() {

  const [count, setCount] = useState(0);
  const [listOfUsers, setListOfUsers] = useState(["Bob", "Terry"]);

  // Calucation called whenever component is rendered / re-rendered 
  const calculation = someExpensiveComputation(count);

  const increment = () => {
    setCount((i) => i + 1);
  };

  const addUser = useCallback(() => {
    setListOfUsers((prev) => [...prev, "Anonymous"]);
  }, [listOfUsers]);

  return (
    <div className='App'>
      <ChildComponent listOfUsers={listOfUsers} addUser={addUser}/>
      <div>
        Number count: {count}
        <br/>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default App;

Updating either count or listOfUsers would trigger a re-render of the App component which in return would call the someExpensiveComputation function each time.

After:

import './App.css';

// import useMemo
import { useState, useCallback, useMemo} from 'react';
import ChildComponent from './ChildComponent';

const someExpensiveComputation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 500000000; i++) {
    num *= 2 % 5;
  }
  return num;
};

function App() {

  const [count, setCount] = useState(0);
  const [listOfUsers, setListOfUsers] = useState(["Bob", "Terry"]);

  // Implement useMemo with count as a dependency
  const calculation = useMemo(() =>someExpensiveComputation(count), [count]);

  const increment = () => {
    setCount((i) => i + 1);
  };

  const addUser = useCallback(() => {
    setListOfUsers((prev) => [...prev, "Anonymous"]);
  }, [listOfUsers]);

  return (
    <div className='App'>
      <ChildComponent listOfUsers={listOfUsers} addUser={addUser}/>
      <div>
        Number count: {count}
        <br/>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default App;

Now useMemo caches the result of someExpensiveComputation. As count is the only dependency, as count is used in the calculations, when the App component re-renders when listOfUsers is updated, useMemo will simply return the cached result instead of performing the same calucations again improving performance in this scenario.

How React will handle this for you in version 19.

We’ve taken a look at various memoization hooks provided by React such as memo, useMemo and useCallBacksthat offer a way to optimize performance by minimizing redundances in re-rendering and re-calculations. It is not without it’s drawbacks too however. It requires additional code to implement which in return clutters up code and introduces more areas for code to go wrong.

However, React 19 (yet to be released as of this post) promises to introduce their own compiler which will implement memoization in areas where it deems appropriate. This means that you’ll have less code to worry about and more likely than not, the compiler will also pick up on areas of code that can be memoized that you may miss when manually implementing yourself.