memo を使うと、propsが前回と同じときに、コンポーネントは再レンダリングを飛ばせます。コンポーネントを memo(...) で包むと、親がレンダリングされたとき、Reactはまず新しい各propを古いものと比べ、すべて等しければ、コンポーネントをもう一度呼ぶ代わりに前回の結果を再利用します。
入力欄に入力してください。入力はもたつかず、コンソールに render SlowList が出るのは最初のレンダリングの1回だけです。memo( の包み(と閉じかっこ)を削除してもう一度入力すると、キーを押すたびにリストのために150ミリ秒待たされ、レンダリングが記録されます。
構文
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
SomeComponentは任意の関数コンポーネントです。よくある書き方は、定義を直接包むことです:const Card = memo(function Card(props) { ... })。中の関数に名前を付けておくと、React DevToolsやエラーメッセージにその名前が残ります。arePropsEqualは省略可能で、必要になることはまれです(後述)。- 結果はほかのものと同じように描画するコンポーネントです。
デフォルトでは、Reactは古いpropsと新しいpropsを浅く比べます。各propを Object.is で確認するのです。文字列、数値、真偽値は値で比べられるので、title="Hi" は title="Hi" と等しくなります。オブジェクト、配列、関数は参照で比べられ、中身が同じ2つのオブジェクトでも別のオブジェクトです。
上の例では items がコンポーネントの外で宣言されているので、すべてのレンダリングで同じ配列になり、比較が通ります。
新しいオブジェクトと関数はmemoを打ち消す
親の本体の中にリテラルとして書いたものは、レンダリングのたびに作り直されます:{ name: 'Ada' }、[1, 2, 3]、() => save()。それをmemoのコンポーネントに渡すと、そのpropは前回のものと決して等しくなりません。
クリックのたびに render Badge object prop が記録されますが、文字列版は一度も記録されません。オブジェクトの場合の直し方は3つあります。
- プリミティブを渡す。子がフィールドを1つか2つしか必要としないなら、
user={{ name: 'Ada' }}ではなくname="Ada"を渡します。 - 定数をコンポーネントの外に移す。決して変わらないオブジェクトは、最初の例の
itemsのようにモジュールのトップレベルに置けます。 - キャッシュする。propsやstateから作るオブジェクトや配列はuseMemoで、関数はuseCallbackで包みます。
children にも同じ問題があります。<Card><p>Hello</p></Card> はレンダリングのたびに新しい <p> 要素のオブジェクトを作るので、childrenを受け取るmemoの Card は毎回再レンダリングされます。
memoが止めないもの
memo が見るのは、親から来るpropsだけです。メモ化したコンポーネントも、次のときにはレンダリングされます。
- 自分のstateが変わったとき(自分の
useStateやuseReducerを通じて) - 読んでいるコンテキストが変わったとき(
useContextを通じて)
Unrelatedをクリックすると、Panel は飛ばされます。Switch themeではコンテキストを読んでいるのでレンダリングされます。Likeをクリックすると、自分のstateが変わったのでレンダリングされます。これは意図された動きです。memo は、コンポーネントが依存するものが何も変わっていないときに作業を省くためのもので、コンテキストとstateはそのコンポーネントが依存するものなのです。
独自の比較関数
第2引数は浅い比較を置き換えます。前回と次回のpropsを受け取り、等しいと見なすべきとき、つまりReactにレンダリングを飛ばさせるときに true を返します。
const MemoChart = memo(Chart, (prev, next) => {
return (
prev.points.length === next.points.length &&
prev.points.every((p, i) => p === next.points[i]) &&
prev.color === next.color &&
prev.onSelect === next.onSelect
);
});
これが必要になることはまれで、間違えやすいものです。関数があるpropを無視すると、コンポーネントはそのpropの古い値を表示し続けます。関数のpropを無視するのが最悪のケースで、飛ばされたコンポーネントは古い関数を呼び続け、その関数は古いstateを読みます。すべてのpropを比べ、大きなデータに深い比較をしないでください。比較のコストが、省くレンダリングのコストを上回ってしまいます。ほぼ常に、useMemo と useCallback でpropsを安定させるほうがよい直し方です。
memoが効いているか確かめる
一番手早い確認方法は、このページで使っている方法です。コンポーネントの先頭に console.log を置きます。飛ばされるはずのときに出力されたら、propsもログに出し、前回のものと比べて変わったものを探します。実際のプロジェクトでは、React DevToolsがさらに役立ちます。設定で「Highlight updates when components render」を有効にすると、操作のたびにどのコンポーネントが光るかがわかり、Profilerタブはすべてのレンダリングを記録し、対応する設定を有効にすれば各コンポーネントがなぜレンダリングされたかも記録します。
memoが失敗しているように見えて、実はそうでないことが2つあります。
- 最初のレンダリングは必ず起きる。
memoがレンダリングを飛ばすのは、コンポーネントがそのpropsで1回レンダリングされた後だけです。 - 開発ビルドではより多くレンダリングされることがある。StrictModeでは、純粋でないコードを見つけるために、Reactは開発中に各コンポーネントを2回レンダリングします。ここのプレビューは本番ビルドのように動くので、上の各ログ行は実際のレンダリング1回を意味します。
memoを使うべきとき
memo が価値を持つのは、コンポーネントのレンダリングが遅く、親がそのコンポーネントに同じpropsを渡してレンダリングされることが多いときです。入力欄の隣にある長いリストやチャート、毎秒更新されるページの中のサイドバーなどです。すばやくレンダリングされるコンポーネントや、どのみちpropsが毎回変わるコンポーネントでは、比較が増えるだけで何も得られません。メモ化する前に、レンダリングのページで紹介しているように、stateを下へ移したりJSXをchildrenとして渡したりして、構造を変えられないかを確かめてください。
プロジェクトでReact Compiler 1.0を使っていれば、ビルド時にコンポーネントとそのpropsが自動でメモ化されるので、手書きの memo 呼び出しのほとんどは不要になります。コンパイラがなければ、memo はここで説明したとおりに動き、Reactのコードで今もよく使われています。
よくある質問
React.memoは何をするものですか?
コンポーネントのメモ化された版を返します。親がレンダリングされると、Reactは新しいpropsを前回のものと比べ、すべてのpropが Object.is で等しければ、そのコンポーネントのレンダリングを飛ばします。
memoしたコンポーネントがそれでも再レンダリングされるのはなぜですか?
たいていは、あるpropが親のレンダリング中に作られた新しいオブジェクト、配列、関数で、前回のものと決して等しくならないからです。また、memoであってもなくても、自分のstateが変わったときや、読んでいるコンテキストが変わったときにはレンダリングされます。
React.memoとuseMemoの違いは何ですか?
memo はコンポーネントを包み、そのレンダリングを飛ばします。useMemo はコンポーネントの中で計算した値をキャッシュするフックです。この2つはよく一緒に使われます。useMemo がオブジェクトのpropを安定させ、memo が飛ばせるようにするのです。
すべてのコンポーネントをmemoで包むべきですか?
いいえ。ほとんどのレンダリングは軽く、propsの比較にもコストがあります。レンダリングが遅く、同じpropsを受け取ることが多いコンポーネントに使ってください。
memoはchildrenと一緒に使えますか?
ほとんど使えません。children として渡すJSXはレンダリングのたびに新しいオブジェクトになるので、childrenを受け取るmemoのコンポーネントはほぼ毎回再レンダリングされます。