React memo() vs useMemo() vs useCallback()

ยท

4 min read

Think of memoization as caching a value/Component so that it does not need to be recalculated/re-rendered.
All of them can improve performance, but let's dive into each one to find out the differences:

memo():

memo() is a Higher Order Component. It will cause React to skip rendering a component if its props have not changed.

Problem is:
In this example, Countries component re-renders even if countries prop have not changed ( open console on devtools and see the child component logs ):

import {useState} from 'react';
import Countries from "./Countries";

export default function App() {
  const [count,setCount] = useState(0);
  const [countries,setCountries] = useState(['Iran','china'])
  return (
    <div className="App">
      <h1>Parent Component</h1>
      <div>Count: {count}</div>
      <button onClick={()=>setCount(c=>c+1)}>Increment</button>
      <Countries countries={countries}/>
    </div>
  );
}
// child component
const Countries = ({ countries }) => {
  console.log("Rendering Child Component...");
  return (
    <>
      <h1>Countries</h1>
      {countries?.map((country, index) => (
        <div key={index}>{country}</div>
      ))}
    </>
  );
};
export default Countries;

Solution is:

To fix this, we can use memo(). Wrap the Countriescomponent export in memo:

// Countries.js
import {memo} from 'react';
// rest
export default memo(Countries) //here

useMemo():

It returns a memoized value.

Let's find out what that means in an example.

Problem is:

In this example the calculation function is artificially slowed down so that you can see what happens when you increase and update count state or even add a country because of rendering on each event:

import { useState } from "react";

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

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

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setCountries((c) => [...c, "Iran"]);
  };

  return (
    <div>
      <div>
        <h2>Countries</h2>
        {countries.map((country, index) => {
          return <p key={index}>{country}</p>;
        })}
        <button onClick={addTodo}>Add Country</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Calculation</h2>
        {calculation}
      </div>
    </div>
  );
}

Solution is:
To fix this performance issue, we can use the useMemo to memoize the calculation function. This will cause the function to only run when count changed for example.

import { useState, useMemo } from "react";
// rest
const calculating = useMemo(() => calculation(count), [count]); //here
// rest

Now, you can see there is no delay when you add country. But not when count changed.


useCallback():

It returns a memoized callback function.

Note: The useCallback and useMemo Hooks are similar. The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.

Back to the first section. memo() prevented the re-rendering of Countries component.

But what if we pass another prop called addCountry:

import { useState } from "react";
import Countries from "./Countries";

export default function App() {
  const [count, setCount] = useState(0);
  const [countries, setCountries] = useState(["Iran", "china"]);

  const addCountry = () => {
    setCountries((c) => [...c, "Japan"]);
  };

  return (
    <div className="App">
      <h1>Parent Component</h1>
      <div>Count: {count}</div>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <Countries countries={countries} addCountry={addCountry} />
    </div>
  );
}
// Child Component
import { memo } from "react";

const Countries = ({ countries, addCountry }) => {
  console.log("Rendering Child Component...");
  return (
    <>
      <h1>Countries</h1>
      {countries?.map((country, index) => (
        <div key={index}>{country}</div>
      ))}
      <button onClick={addCountry}>Add Country</button>
    </>
  );
};
export default memo(Countries);

Problem is:

Try to click Increment button to update count state. You can see Countries component re-rendered again although we wrapped it with memo().

Solution is:
To fix this, we can use the useCallback hook to prevent addCountry from being recreated unless necessary. It is enough to change addCountry function to this:

// App.js
import { useState, useCallback } from "react";
// rest
 const addCountry = useCallback(() => {
    setCountries((c) => [...c, "Japan"]);
  }, []);
// rest

Now the Countries component re-rendered only when countries prop updated.


I hope this article helps anyone who confused using these hooks.
Thanks for your time ๐Ÿ™๐Ÿฅฑ.

ย