Menu

React useMemoの使い方:重い計算をキャッシュする

useMemoは計算結果をレンダリング間でキャッシュし、依存値が変わったときだけ計算し直します。構文、memoの子のためにオブジェクトを安定させる方法、そしてuseMemoを使う価値がない場面を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

useMemo は計算結果をレンダリング間でキャッシュします。Reactは最初のレンダリングで関数を実行して結果を保ち、依存配列の値のどれかが変わるまで、以降のレンダリングではその同じ結果を返します。

コンソールを開いて、テーマを何回か切り替えてください。何も記録されず、切り替えは一瞬です。タブを切り替えると、少し間をおいて filtering for active が表示されます。tab が依存値だからです。次に useMemo の行を const visible = filterTodos(todos, tab); に置き換えて、もう一度テーマを切り替えてください。クリックのたびにログが出て、100ミリ秒止まります。

構文

const cachedValue = useMemo(calculateValue, dependencies);

const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  • calculateValue は、キャッシュしたい値を返す引数なしの関数です。Reactはレンダリング中にそれを呼びます。
  • dependencies は、関数が読むコンポーネント内のすべての値(props、state、コンポーネント本体で宣言した変数)のリストです。
  • Reactはレンダリングのたびに、各依存値を前回のレンダリングの値と Object.is で比べます。すべて一致すれば、関数を呼ばずにキャッシュした値を返します。どれかが違えば、関数を呼んで新しい結果をキャッシュします。

useMemo はフックなので、フックのルールに従います。コンポーネントのトップレベルで呼び、ループや条件分岐の中では呼びません。

依存配列は漏れなく書く

配列から値を1つ外すと、キャッシュはその値の古い状態から計算した結果を返し続けます。ここでは合計が qty を読んでいるのに、price しか並べていません。

Quantityをクリックしてください。2になりますが、合計は10のままで、何も記録されません。Priceをクリックすると、合計は22に跳ね上がります。そこで初めて計算が再実行され、現在の qty を読むからです。配列に qty を追加すると、どちらのボタンでも合計が更新されます。実際のプロジェクトでは、react-hooks/exhaustive-deps のlintルールがこの間違いを検出します。

逆の間違いは、コンポーネント本体で作ったオブジェクトのように、レンダリングのたびに新しくなる依存値です。前回の値と一致することがないので、キャッシュは決して使われません。

const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time

// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);

memoの子のためにオブジェクトを安定させる

useMemo を使う2つ目の理由は、遅い計算とは関係ありません。memoで包んだ子がレンダリングを飛ばすのは、すべてのpropが前回と同じときだけです。コンポーネント本体に書いたオブジェクトリテラルはレンダリングのたびに新しいオブジェクトになるので、中身が変わっていなくても memo の効果を打ち消してしまいます。useMemo は入力が変わるまで同じオブジェクトを保ちます。

メモ欄に入力しても Chart は静かなままで、Switch unitでレンダリングが1回記録されます。useMemo の呼び出しを普通の const options = { unit, max: unit === 'km' ? 100 : 62 }; に置き換えると、メモ欄でキーを押すたびに render Chart が記録されます。

オブジェクトに依存するエフェクトにも同じことが言えます。エフェクトが依存値に options を並べていて、options がレンダリングのたびに作り直されると、エフェクトはレンダリングのたびに実行されます。useMemo で直せますが、多くの場合もっと簡単なのは、オブジェクトをエフェクトの中で作り、プリミティブな値に依存させることです。

useEffect(() => {
    const options = { serverUrl, roomId };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
}, [serverUrl, roomId]);

useMemoを使うべきでないとき

コンポーネントの中のほとんどの計算は軽いものです。数値の足し算、2つの文字列の連結、数十個の項目のフィルターなどです。それらをキャッシュしても節約はほぼゼロです。Reactはそれでもレンダリングのたびに値を保存して依存値を比べ、コードも読みにくくなります。

// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;

何かが遅いかどうかを知るには、時間を測ってください。計算を console.time('filter') と console.timeEnd('filter') で囲み、操作を実行して数値を見ます。おおまかな目安として、自分のマシンで1ミリ秒以上かかる処理が候補ですが、ユーザーの端末はもっと遅いかもしれないことを忘れないでください。useMemo に手を伸ばす前に、その処理がそもそもレンダリングに属するかも確かめてください。決して変わらないデータはコンポーネントの外で1回計算でき、ページのごく一部しか使わないstateはより小さなコンポーネントに移せます(レンダリングを参照)。

派生した値にはuseEffectではなくuseMemo

よくあるパターンは、値をエフェクトで計算してstateに保存することです。

// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
    setVisible(filterTodos(todos, tab));
}, [todos, tab]);

// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);

エフェクト版は古い visible で1回レンダリングし、それからエフェクトを実行し、stateをセットして、もう一度レンダリングします。余分なレンダリングが1回増え、その最初のレンダリングではリストがタブと一致していません。useMemo で(処理が軽ければフックなしで)レンダリング中に計算すれば、同じレンダリングで正しい値が得られます。エフェクトはReactの外のものと同期するためのもので、あるstateから別のstateを導くためのものではありません。

保証ではなくパフォーマンスのヒント

useMemo は、結果を再利用してもよいとReactに伝えるものです。再利用すると約束するものではありません。通常の動作ではReactはキャッシュした値を保ちますが、開発中にファイルを編集したときや、コンポーネントが最初のマウント中にサスペンドしたときなどに、それを捨てることがあります。計算が再実行されても正しくなるようにコードを書いてください。関数は純粋でなければならず、リクエスト、購読、外の変数の変更といった副作用を持ってはいけません。副作用はイベントハンドラかエフェクトに書きます。

開発中のStrictModeでは、純粋でないコードを見つけやすくするために、Reactは計算の関数を2回呼びます。ここのプレビューは本番ビルドのように動くので、各 filtering for のログは1回ずつ表示されます。

useMemoとReact Compiler

React Compiler 1.0は、コンポーネントを解析してメモ化を自動で追加するビルドツールで、useMemo、useCallback、memo が役立つ場所で計算やJSXをキャッシュします。これを使うプロジェクトでは、手で書く回数は減るでしょう。手書きの useMemo はコンパイラと並んで今も動き、コンパイラを導入していないときの道具でもあり、既存のReactのコードのほとんどで目にします。

よくある質問

ReactのuseMemoは何をするものですか?

最初のレンダリングで計算の関数を呼んで結果を保存し、以降のレンダリングでは、依存配列のすべての値が前回と同じである限り(Object.is で比較)、保存した結果を返します。

useMemoはいつ使うべきですか?

計算が目に見えて遅く、その入力がめったに変わらないとき、または memo の子やエフェクトが依存しているために、オブジェクトや配列にレンダリング間で同じ参照を保たせる必要があるときです。軽い処理なら使わないでください。

useMemoとuseCallbackの違いは何ですか?

useMemo は関数が返す値をキャッシュします。useCallback は関数そのものをキャッシュします。useCallback(fn, deps) は useMemo(() => fn, deps) と同じです。

useMemoを使えば値が再計算されないことが保証されますか?

いいえ。これはパフォーマンスのためのヒントです。開発中にファイルを編集したときなど、Reactがキャッシュした値を捨てる場合があるので、計算がもう一度実行されてもコードが正しく動く必要があります。

React Compilerがあれば、useMemoはもう不要ですか?

ビルドでReact Compilerを使っていれば、メモ化を自動で追加してくれます。コンパイラがない場合や、コンパイラが対象外とするケースでは、手書きの useMemo が今も同じように動き、既存のコードでもよく使われています。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める