Memoisation in React

parkrooben

Benjamin Liu

Posted on March 13, 2021

Memoisation in React

Memoisation is an optimisation technique that caches the result of previous computations so that they can be quickly accessed without repeating the same computation.

React introduces quite a few memoisation functions being React.memo, useMemo and useCallback.

1. React.memo

React.memo is a higher order component when wrapped around a component, memoises the result of the component and does a shallow comparison before the next render. If the new props are the same the component doesn't re-render and uses the memoised result.

By default memo does a shallow comparison of props, however, the second argument allows you to define a custom equality check function. From React's official docs:

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);
Enter fullscreen mode Exit fullscreen mode

However, if you're looking to do a deep comparison between 2 values and want to take the easy route you can use isEqual from lodash.

Now let's have a look at this example:

// App.js
import React, { useState } from "react";

import Child from "./Child.js";

const App = () => {
  const [counter, setCounter] = useState(0);
  const [text, setText] = useState("");

  return (
    <div className="App">
      <input
        onChange={(e) => setText(e.target.value)}
        type="text"
        value={text}
      />
      <button onClick={() => setCounter(counter + 1)}>+ 1</button>
      <Child counter={counter} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this case we have a parent component called App which takes in a <Child /> component.

import React from "react";

const Child = ({ counter }) => {
  console.log("rendering...");

  return <div>Count: {counter}</div>;
};

export default Child;

Enter fullscreen mode Exit fullscreen mode

If you open up Console you will notice that given each keystroke in the input field the <Child /> component re-renders. Obviously this doesn't have any performance overhead at this point in time, but imagine if the Child component had child components of its own with state. Then you'd trigger a re-render of all components associated with the parent, that'd definitely add overhead to your application.

To prevent child components from unnecessarily re-rendering like that we have to use React.memo. All we need to do is wrap our Child component in our memo and you see that no matter what we type in the input field it doesn't trigger a re-render of the <Child /> component.

import React, { memo } from "react";

const Child = ({ counter }) => {
  console.log("rendering...");

  return <div>Count: {counter}</div>;
};

export default memo(Child);
Enter fullscreen mode Exit fullscreen mode

However, what if we wanted to pass down functions or anything that isn't a primitive value such as objects since memo only does a shallow comparison? A shallow comparison in this case means it only checks if the props that you're passing down is referencing the same place in memory.

So let's say we want to update the counter from <Child /> so we do something like this:

// App.js
import React, { useState } from "react";

import Child from "./Child.js";

const App = () => {
  const [counter, setCounter] = useState(0);
  const [text, setText] = useState("");

  const updateCounterHandler = () => {
    setCounter(counter + 1);
  };

  return (
    <div className="App">
      <input
        onChange={(e) => setText(e.target.value)}
        type="text"
        value={text}
      />
      <button onClick={() => setCounter(counter + 1)}>+ 1</button>
      <Child counter={counter} updateCounter={updateCounterHandler} />
    </div>
  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

and within Child.js:

import React, { memo } from "react";

const Child = ({ counter, updateCounter: pushUpdateCounter }) => {
  console.log("rendering...");

  return (
    <div>
      <strong>Count: {counter}</strong>
      <button onClick={pushUpdateCounter}>Update Counter</button>
    </div>
  );
};

export default memo(Child);

Enter fullscreen mode Exit fullscreen mode

However, you will notice that the <Child /> component still gets rendered whenever we type something into the input field. This is because the updateCounterHandler inside App.js gets recreated each time the state changes.

So the correct way to handle callback functions with memo is by using useCallback.

2. useCallback

useCallback is a hook that comes with react that returns a memoised function. It takes in 2 arguments, the first one being the callback function, the second being an array of dependencies.

So all that needs to be done is wrap useCallback around our updateCounterHandler function to prevent the <Child /> component from re-rendering whenever we type in the input field.

const updateCounterHandler = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);
Enter fullscreen mode Exit fullscreen mode

3. useMemo

Like useCallback, useMemo is a hook that takes in a function, however, instead of returning a memoised function it returns a memoised value. This makes it useful when performing heavy calculations.

import React, { memo, useMemo } from "react";

const Child = ({ counter, updateCounter: pushUpdateCounter }) => {
  console.log("rendering...");

  const outputNumber = useMemo(() => {
    let output = 0;

    for (let i = 0; i < 10000000; i++) {
      output++;
    }

    return output;
  }, []);

  return (
    <div>
      <strong>Count: {counter}</strong>
      <div>Output Number: {outputNumber}</div>
      <button onClick={pushUpdateCounter}>Update Counter</button>
    </div>
  );
};

export default memo(Child);
Enter fullscreen mode Exit fullscreen mode

Using useMemo in the example above, we're able to cache the return value of outputNumber, so that we're not recalling the function each time.


After learning these techniques, I hope you're able to apply it to where it's truly needed, because premature optimisation is the root of all evil! It's about finding the fine line between compromising space and time as speed optimisation techniques such as memoisation eat up space (RAM) in return for a faster time. So always question yourself before optimising your code, "do the performance gains really justify the usage?".

đź’– đź’Ş đź™… đźš©
parkrooben
Benjamin Liu

Posted on March 13, 2021

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

Sign up to receive the latest update from our blog.

Related