🪬

useMemo, useCallback, React.memoとは何か?違いは?

2021/05/15に公開

はじめに

React で useMemo や useCallback という hooks をよく耳にしますが、メモ化する関数としか認識できていません。
そのため今回は、具体的な実装や違いなどについて理解していけたらなと思って書いていきます。

useMemo や useCallback を検索すると多くの方が記事を書いていているので、具体的な実装例など詳しくはそちらにお任せして、この記事では概要というか要点だけをまとめていこうかと思います。

「そもそもメモ化とはなにか」という方はこちらにまとめたので確認してみてください。

https://zenn.dev/yyykms123/articles/2021-05-14-memoization

パフォーマンスチューニング

まず useMemo などを理解する前の前提として、パフォーマンスチューニングについて軽く触れたいと思います。

パフォーマンスチューニングは広義に捉えると、 「システムの処理性能を高めるために、システムの動作を最適化すること」 です。

React におけるパフォーマンスチューニングとは、 「無駄な計算や再レンダリングの負荷を減らして動作を最適化すること」 と考えていいかなと思います。

React は親コンポーネントや state が更新されるとそのコンポーネント配下の全コンポーネントが再レンダリングされます。

そこで、 useMemo などを使用して再レンダリングさせないタイミングをホワイトリスト形式でコンポーネントごとに定義していきます。

もっと具体的に詳しく知りたい方はこちらの記事を参考にしてみてください。

https://qiita.com/teradonburi/items/5b8f79d26e1b319ac44f

メモ化する3つの方法

メモ化するものとして 3 つ存在します。

  1. React.memo
  2. useCallback
  3. useMemo

メモ化する対象は

React.memo, useCallback, useMemo はそれぞれメモ化する対象が異なります。

また useCallback, useMemo は hook ですが、 React.memo に関してはメモ化したコンポーネントを返す HOC(higher-order component) となっています。

名前 メモ化する対象 種類
React.memo コンポーネント HOC
useCallback コールバック関数 hook
useMemo 計算結果の値 hook

メモ化の歴史

それぞれを理解する前に、パフォーマンスチューニングの歴史を知るとより理解できると思うので、ここで軽く紹介します。

クラスコンポーネントでは shouldComponentUpdate というメソッドをオーバーライドして、引数に props を指定し、新旧を比較することで再レンダリングするかを制御していました。

しかし、 props が増えるたびに比較する処理を記述していく必要があったため、それを解決するために、自動で比較してくれる PureComponent というコンポーネントが誕生しました。

こうしてクラスコンポーネントでは比較処理コストや再レンダリングコストの大小によって 2 つを使い分けて、パフォーマンスチューニングを行っていたようです。

ただ、関数コンポーネントではそういった機能を用意していなかったのでチューニングができず再レンダリングされていました。

しかし、 React v16.6 から React.memo が追加され、関数コンポーネントでも shouldComponentUpdate と同様の機能が使えるようになりました。

そして、 React v16.8 から Hooks が追加され、useMemouseCallback によってさらにパフォーマンスチューニングがしやすくなりました。

参考

React.memo

https://ja.react.dev/reference/react/memo

React.memo は前述のように、コンポーネントをメモ化する HOC で、 props を受け取り新旧を比較して再レンダリングするかを制御します。

しかしコールバック関数を props で受け取った時は再レンダリングされてしまいます。

関数はコンポーネントが再レンダリングされる度に新しいオブジェクトとして生成されるので、 props として受け取ると等価として評価されずに再レンダリングされてしまうのです。

そこで、同一の関数オブジェクトとするために useCallback を使用して、 props に渡す関数をメモ化することでこの現象を回避できます。

useCallback

https://ja.react.dev/reference/react/useCallback

useCallback はメモ化したコールバック関数を返します。

React.memo との併用が主な用途で、単体ではあまり使用しないらしいです。
( useCallback を実行するコストより、関数を再生成するコストの方が大きくなる局面はほぼないらしい ※後述します )

useCallback の注意点として避けるべきことが 2 点あります。

  • React.memo でメモ化をしていないコンポーネントに useCallback でメモ化をしたコールバック関数を渡す
    • メモ化をしていないコンポーネントに、メモ化した関数を渡しても再レンダリングされる
  • useCallback でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する
    • 関数自体は実行できますが、再レンダリングさせないということはできません

useMemo

https://ja.react.dev/reference/react/useMemo

useMemo は計算結果の値をメモ化します。
不要な再計算を避けることでパフォーマンスを上げます。

なお、 useMemo はレンダリング結果もメモ化できるので、 React.memo と同じようにコンポーネントの再レンダリングを制御できます。

具体例

具体例は自分がとてもわかりやすいと思った記事のリンクを貼っておきます。

こちらを参考にしてみてください。

useCallbackの単体での利用について

useCallback は前述のように、 React.memo との併用が主な用途で、単体ではあまり使用しないらしいです。

理由としては「関数を再生成するコスト > useCallback の実行コスト単体」となる局面がほぼないとのことです。

しかし、 useCallback 単体で使用している例を見かけたことがあるので、ここで紹介しておきます。

const [users, setUsers] = useState<User[]>([])

const getUsers = useCallback(async () => {
  try {
    const response = await axios.get<{ users: User[] }>('/api/users')
    if (isMountedRef.current) {
      setUsers(response.data.users)
    }
  } catch (err) {
    console.error(err)
  }
}, [isMountedRef])

useEffect(() => {
  getUsers()
}, [getUsers])

useCallback を使用すると、関数の再生成をしないため、中の処理を何回も実行しないで済むようになっています。

このように一応単体での使用例もあるみたいです。

React.memo と useMemo の違い

React.memo はコンポーネントをメモ化します。

useMemo は計算結果の値をメモ化しますが、コンポーネントのレンダリング結果のメモ化もできるので、 React.memo と同じように実装できます。

そこで、これらの違いとは?どういう使い分け?という疑問が浮かんだのですが、具体的な例があまり出てこなかったです。

違いとしては HOC と hook なので、用途によって違うということでしょうか。

コメントなどで意見や具体例をいただけると助かります。

さいごに

React におけるパフォーマンスチューニングは、3 つを使用することで実装できます。

名 前 メモ化する対象 種類
React.memo コンポーネント HOC
useCallback コールバック関数 hook
useMemo 計算結果の値 hook

具体的な実装例はあえて書かずに文章だけで説明をしたので、理解ができないところは参考記事をよくみてみてください。

今後 React の実装でパフォーマンスを考えた実装をするために、状況に応じてそれぞれ使い分けていければと思います。

Discussion