React.memo/useMemo/useCallbackの学習
今回はReact.memo/useMemo/useCallbackについて学習します。
参考にしたのは下記のYoutube。
【ShinCode camp】レンダリングを最適化してWebパフォーマンスを向上させてみよう
目次
- React developer toolの導入
- React.memoの使い方
- useMemoの使い方
- useCallbackの使い方
1. React developer toolの導入
レンダリングしている箇所を把握するため、React developer tool(chromeの拡張機能)を導入します。
開発者ツールを開いて、Componentsを選択した後、「Higlight updates when components render.」にチェックを入れる
レンダリングされた場合、レンダリング箇所が水色の枠線で囲まれます。
useStateで文字を入力するたびにレンダリングされていることが確認できます。
※先ほど選択したComponentsではなく、Profilerのタブで何秒かかったか詳細を確認できるようです。
2. React.memoの使い方
通常、親コンポーネントが更新されると、それにぶら下がっている子コンポーネントについても一緒に再レンダリングされてしまいます。
React.memoを使うと、子コンポーネントが再レンダリングされてしまうことを防ぐことができます。
下記のような単純な親コンポーネントと子コンポーネントを作成します。
const Child1 = () => {
return (
<>
<p>子コンポーネントです。</p>
</>
);
};
export default function Parent() {
const [text, setText] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
return (
<div>
<p>親コンポーネントです</p>
<input
type="text"
onChange={handleChange}
value={text}
className="border-2 border-slate-200 rounded-md"
/>
<Child1 />
</div>
);
}
先ほどのツールで親コンポーネントの更新時に、子コンポーネントもレンダリングされていることを確認。
静止画のみだと少し分かりにくいですが、親コンポーネントを更新(input欄に文字を入力、または削除)すると、Child1コンポーネントの部分も更新されています。
次に、子コンポーネントをReact.memo()で囲ってやります。
const Child1 = React.memo(() => {
return (
<>
<p>子コンポーネントです。</p>
</>
);
});
子コンポーネントは、親コンポーネントを更新してもレンダリングされないことが確認できました。(先ほどと違い「Child1」の表示なし)
3. useMemoの使い方
useMemoを使うと、今度は関数の無駄な再レンダリングを防ぐことができます。
分かりやすように、下記のコードのようなwhile文で何回も繰り返すような重い計算処理を追加します。
while文の中身は、親コンポーネントがレンダリングされるたび(今回の場合、input欄に文字入力を行うたび)に重い計算処理の部分が実行されることになります。
export default function Parent() {
const [text, setText] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// 重い計算処理の部分 ここから
const [count, setCount] = useState(0);
const double = (count: number) => {
let i = 0;
while (i < 30000000) {
i++;
}
return count * 2;
};
const doubleCount = double(count);
// ここまで
return (
<div>
<p>親コンポーネントです</p>
<input
type="text"
onChange={handleChange}
value={text}
className="border-2 border-slate-200 rounded-md"
/>
<p>親コンポーネントで重い計算処理</p>
<p>
Counter: {count}, {doubleCount}
</p>
<button
className="border-2 rounded-md"
onClick={() => setCount(count + 1)}
>
Increment Count2
</button>
</div>
);
}
<実行結果>
useMemoを下記のようにdouble関数の部分に適用します。
このとき、第二引数に[count]を指定します。
こうすると、countが更新されたときのみdouble関数の部分を実行し、countが更新されていない場合はこの処理はスキップされます。
const doubleCount = useMemo(() => double(count), [count]);
<実行結果>
親コンポーネントを更新(input欄に文字を入力)しても、レンダリング時間が増えなくなりました。
4. useCallbackの使い方
useCallbackを使うと、関数の再生成を防ぐことができます。
useMemoと何が違うの?とちょっと思ったんですが、
- useMemo・・・計算結果が同じ場合にキャッシュ(メモ化)する
- useCallback・・・関数自体の再生成を防ぐ(関数の中身が同じなら同じ関数を再利用する)
といった違いがあるようです。
// Child1コンポーネントにpropsとしてhandleClickを渡す
const Child1 = React.memo((props: { handleClick: () => void }) => {
return (
<>
<p>Child1コンポーネントです。</p>
<button className="border-2 rounded-md p-1" onClick={props.handleClick}>
Click
</button>
</>
);
});
export default function Parent() {
const [text, setText] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// handleClick関数を追加
const handleClick = () => {
console.log("click");
};
// ここまで
return (
<div>
<p>親コンポーネントです</p>
<input
type="text"
onChange={handleChange}
value={text}
className="border-2 border-slate-200 rounded-md"
/>
<p></p>
<br />
<Child1 handleClick={handleClick} />
</div>
);
}
<実行結果>
このケースでは、親コンポーネントがレンダリングされるたびにhandleClick関数が再生成され、それをpropsとして受け取っているChild1も再レンダリングされます。
(上記の結果でAnonymousと表示されている部分でChild1が再生成されています)
これを防ぐため、useCallbackをhandleClickに適用してやります。
const handleClick = useCallback(() => {
console.log("click");
}, []);
第二引数は基本は空配列にしておきます。
こうすると最初にページがマウントされたときのみレンダリングが行われます。
※仮に[text]を入力すると、textが変更されるたびにレンダリングが行われるので、今回のケースでは意味がなくなります。
<実行結果>
親コンポーネントを更新しても、Child1コンポーネント(Anonymousの表示)が再レンダリングされることはなくなりました。
所感
注意点として、全てに対してuseMemo、useCallbackを使うのではなく、ボトルネックとなっている箇所を特定してメモ化を行うのが推奨されるようです。
(メモ化するだけでもメモリを使ったりするので逆効果になるケースもあり得るとのこと)
記事の内容だけでは分かりにくかった方は、一度冒頭に紹介した動画も確認してみてください。
非常に分かりやすくまとめられており、今回の内容が理解できるのではと思います。
Discussion