この記事は 『CRESCO Advent Calendar 2022』24日目の記事です。

 

みなさん、こんにちは。AIテクノロジーセンターのあーくです。
Reactのコンポーネントって各Web画面を疎結合化できるので便利ですよね。

そこで積極的に使っていきたい機能にメモ化があるのですが、3種類(React.memo、useCallback、useMemo)もあるので、いざ使いたいときにどこで使ったらいいのか忘れちゃうんですよね。検索しても詳しい使い方が書かれた記事はたくさん見つかるんですが、すぐに知ることができない。(泣)

ということで、すぐに見て分かるように各コードの位置をまとめました!

メモ化の各コードの位置

// 親コンポーネント
export function App() {
// useMemoは、関数コンポーネント内の関数をメモ化
const func = useMemo(() => doSomething(arg), [arg]);
// useCallbackは、React.memoでメモ化したコンポーネントに渡すコールバック関数をメモ化
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<>
// React.memoは、コンポーネントをメモ化
<Component onClick={handleClick} />
<... />
</>
)
}
// 子コンポーネント(React.memoを使用)
export default React.memo(function Component(handleClick) {
return(
<button onClick={handleClick} />
)
});

上記のコメントに各メモ化のコードの説明を簡単に書いています。ただ、これだけ見ても「各メモ化ってどんな機能だったっけ?」となる方もいるかもしれないので、それぞれの機能も一緒に復習していきましょう!

メモ化とは

計算結果を保持し、それを再利用する手法のことです。
メモ化によって各コンポーネントで都度計算する必要がなくなるため、パフォーマンスの向上が期待できます。

メモ化の種類にはReact.memo、useCallback、useMemoの3つがあります。

React.memo

コンポーネント(コンポーネントのレンダリング結果)をメモ化する React の API(メソッド)です。
コンポーネントそのものをメモ化することで、コンポーネントの再レンダリングをスキップできます。

具体的には、以下のようなコンポーネントの再レンダリングをスキップすることで、パフォーマンスの向上が期待できます。

  • レンダリングコストが高いコンポーネント
  • 頻繁に再レンダリングされるコンポーネント内の子コンポーネント

Props の値が同じかどうか(等価性)をチェックして再レンダリングの判断をしています。
新しく渡された Props と前回の Props を比較し、等価であれば再レンダリングをせずにメモ化したコンポーネントを再利用します。

例:

const Child = React.memo((props) => {
let i = 0;
while (i < 1000000000) i++;
console.log("render Child");
return <p>Child: {props.count}</p>;
});

ただ、コールバック関数を Props として受け取ったコンポーネントは React.memo を利用しても必ず再レンダリングされるようになっています。
関数は、コンポーネントが再レンダリングされる度に再生成されるため、等価ではないためです。

「じゃあ関数が毎回生成されないようにするにはどうしたらいいの?」というところで、useCallbackを使います。

useCallback

メモ化されたコールバック関数を返すフックです。

React.memo でメモ化したコンポーネントに useCallback でメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再レンダリングをスキップできます。

例:

import React, { useState, useCallback } from "react";
const Child = React.memo((props) => {
console.log("render Child");
return <button onClick={props.handleClick}>Child</button>;
});
export default function App() {
console.log("render App");
const [count, setCount] = useState(0);
// 関数をメモ化すれば、新しい handleClick と前回の handleClick は
// 等価になる。そのため、Child コンポーネントは再レンダリングされない。
const handleClick = useCallback(() => {
console.log("click");
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handleClick} />
</>
);
}

関数がメモ化できる便利なuseCallbackですが、useCallback は React.memo と併用する必要がある点に注意が必要です。

つまり、以下のような使い方をしても意味がありません(コンポーネントの不要な再レンダリングをスキップできない)。

  • React.memo でメモ化をしていないコンポーネントに useCallback でメモ化をしたコールバック関数を渡す
  • useCallback でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する

無駄にuseCallbackを使っても意味がないので、きれいなコードを書くためには意識しておきたいところです。

最後に、useMemoについて見ていきましょう。

useMemo

メモ化された値を返すフックです。
コンポーネントの再レンダリング時に値を再利用できます。

例:

const doubledCount = useMemo(() => double(count2), [count2]);

useMemo はレンダリング結果もメモ化できるため、React.memo のようにコンポーネントの再レンダリングをスキップできます。

例:

// レンダリング結果(計算結果)をメモ化する
// 第2引数に count2 を渡しているため、count2 が更新された時だけ再レンダリングされる。
// count1 が更新され、コンポーネントが再レンダリングされた時はメモ化したレンダリング結果を
// 利用するため再レンダリングされない。
const Counter = useMemo(() => {
console.log("render Counter");
const doubledCount = double(count2);
return (
<p>
Counter: {count2}, {doubledCount}
</p>
);
}, [count2]);

ueeMemoの注意点としては、関数コンポーネント内でコンポーネントをメモ化したい場合に利用する、というところです。
関数コンポーネント内で useMemoではなく React.memo を利用したとしても意味がありません。

さいごに

Reactのメモ化って慣れないとこんがらがってしまいますよね。開発している最中に記述場所が分からなくなってしまいがちですので、何度も見直して身につけていきましょう!