How to optimise React with useMemo and useCallBack

How to take advantage of useCallBack and useMemo and when to avoid them

analog camera with pictures

Memorization in React is a form of caching. The basic idea is that, once a piece of code runs, the output is going to me memorised and, if the same input is provided, the recalculation is avoided and the same outout will be instantly provided. React has a size 1 cache, this means that only the most recent input/output is stored.

React has three APIs for memorization: memo, useMemo and useCallback.

React Memo

React memo is a high order component that wraps around a normal component and memorise the rendered output. memo will allow React to stop rendering a component if props have not changed.

The following code is a good example why you would use memo. If the user clicks on the + button, the state of the component will change and that will trigger a re-render. This means that, also the child component (the Todo in this case), will re-render, even if it’s content hasn’t changed.

// index.js

import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState(["todo 1", "todo 2"]);

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

  return (
    <>
      <Todos todos={todos} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
// Todos.js

const Todos = ({ todos }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
    </>
  );
};

export default Todos;

Solution: wrapping Todo component with memo will do the trick. In this way, even if the parent component re-rended, the Todo component will not as its state didn’t change.

// Todos.js

import { memo } from "react";

const Todos = ({ todos }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
    </>
  );
};

export default memo(Todos);

useCallback

The useCallback hook returns a memorised function. The hook will only runs if one of the dependencies of the function is updated.

The following scenario is an example when useCallback can be useful to prevent an entire component from re-rendering.

import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
import { memo } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};

export default memo(Todos);

If you consider what we did in the memo example above, you might be surprised that the component keep re-rendering even if it is wrapped inside the memo high-order component.

The reason for the re-rendering is that the addTodo function is recreated every time the component re-render. For this reason, as the function is always a new one, the state of the component Todo component is changing and that triggers the re-rended even if wrapped in memo.

Solution: wrapping the function addTodo in a useCallback hook, will prevent it from be recreated and, consequently, the component to be re-rendered.

// index.js

import { useState, useCallback } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = useCallback(() => {
    setTodos((t) => [...t, "New Todo"]);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
// Todos.js

import { memo } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};

export default memo(Todos);

useMemo

useMemo has a similar behavior as useCallback but instead of returning a memorised function, it returns a memorised value. This can be particularly useful in the presence of resource-expensive functions. If the same input is provided, the function will not run and the memorised optput will be returned.

As useCallback, the hook only runs if one of the dependencies updates.

In the following example, if the + button is pressed, the state will change and this mean that, during the re-rended process, the expensiveCalculation will run every time. This will create a delay between the user pressing the button and the UI updated.

import { useState } from "react";
import ReactDOM from "react-dom/client";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = expensiveCalculation(count);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

Solution: wrapping the expensiveCalculation with useMemo, the function will run only exclusively during the first render cycle. After that, if the component re-render, the funtion won’t run again making the page much more responsive.

import { useState, useMemo } from "react";
import ReactDOM from "react-dom/client";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

Conclusion

Memorisation in React can make a real difference in properly implemented. But in this particular case, the cliché with great power comes great responsibility comes handy. Re-renders are not a problem and we shouldn’t try to avoid them at all costs. Sometimes the memorisation of values when is not necessary can cause more performance drawbacks that advantages.

So, before you start to wrap all your functions in a useCallback, ask yourself if it’s really necessary. If the amount of data you’re handling tiny and the components are already responding instantly to changes, probably it’s not worth to over-complicate things.

Until the next time, keep on coding!

Left arrowArrow icon pointing to the left