useMemo
は、再レンダリング間で計算結果をキャッシュするReactのフックです。
本稿はReact公式サイト「useMemo」にもとづき、useMemo
はどう使うのか、およびどのような場合に使うとよいのかを解説します。説明内容と順序は、公式ドキュメントにしたがいました。ただし、解説はわかりやすく改め、またコード例とサンプル(CodeSandbox)はTypeScriptを加えたうえで修正した部分が少なくありません。
構文
useMemo(calculateValue, dependencies)
useMemo
はコンポーネントのトップレベルで呼び出して、再レンダリング間で計算をキャッシュします。
import { useMemo } from "react";
export const TodoList: FC<Props> = ({ todos, tab }) => {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
};
引数
-
calculateValue
: キャッシュする値を計算する関数。純粋で、引数は取らず、任意の型の値を返さなければなりません。Reactは、まず最初のレンダー時に関数を呼び出し、値が返されます。次回以降のレンダリングについてはつぎのとおりです。- 直前のレンダー時と依存値が変わっていないとき: Reactは前と同じ値を返します。
- 直前のレンダー時から依存値が変わった場合:
calculateValue
が呼び出され、返されるのは新たな結果です。値はあとで使えるように保存されます。
-
dependencies
:CalculateValue
のコード内で参照されるすべてのリアクティブ値の配列。リアクティブな値に含まれるのは、プロパティと状態、およびコンポーネント本体に直接宣言された変数と関数です。React用に設定されたリンターであれば、リアクティブな値がすべて依存関係に正しく指定されているかを確かめます。依存配列には、特定の依存値が要素として含まれなければなりません。インラインで、[dep1, dep2, dep3]
という記述です。Reactは、各依存値を直前の値とObject.is
メソッドで比較します。
戻り値
最初のレンダー時は、calculateValue
の呼び出しによる戻り値です。引数はありません。
以降のレンダーでは、依存値が変わったかどうかにより、返す値は変わります。
- 依存値に変更がない場合: 返されるのは前のレンダリング時に保存した値です。
- 依存値が変わったとき: 改めて
calculateValue
を呼び出し、その戻り値が返されます。
注意
-
useMemo
はフックなので、呼び出せるのはコンポーネントまたはカスタムフックのトップレベルからのみです。ループ文や条件文の中からは呼び出しできません。 -
StrictMode
では、Reactは計算関数を2度呼び出します(後述「再レンダリングのたびに計算が2回実行される」)。コードに純粋でない処理が紛れていないことを確かめるためです。開発環境のみの動作で、本番環境には影響しません。計算関数が(要求どおり)純粋でさえあれば、ロジックに問題も生じないはずです。2回の呼び出しの一方は無視されます。 - Reactは、特別な理由がないかぎり、キャッシュされた値を破棄しません。開発環境ならReactは、たとえばコンポーネントのファイルが編集されたら、キャッシュを破棄します。開発と本番の両環境で、Reactがキャッシュを破棄するのは、コンポーネントの初期マウント完了に至らなかった場合です。Reactは、将来的にキャッシュの破棄が利用できる機能をさらに追加することもありえます。たとえば、仮想化リストの組み込みサポートを加えるようになった場合です。仮想化されたテーブルビューポートからスクロールアウトするアイテムのキャッシュを破棄することは意味があるでしょう。パフォーマンスの最適化が目的で
useMemo
を用いることは問題ありません。そうでない場合は、状態変数やref
を活用する方がより適切なこともありえます。
このように戻り値をキャッシュするのはメモ化(memoization)として知られる技法です。そのため、フックの名前もuseMemo
としました。
使い方
負荷の高い再計算を省く
useMemo
に包むことにより、再レンダーの間で計算をキャッシュします。フックはコンポーネントのトップレベルで呼び出してください。
import { useMemo } from "react";
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
};
useMemo
に渡す引数はふたつです。
- 計算関数: 引数は取りません。計算した結果を返します。
-
() =>
といった構文です。
-
- 依存関係のリスト: 計算で用いられるコンポーネント内のすべての依存値を配列で渡します。
初期レンダー時: useMemo
から得られる値は、計算関数を呼び出した演算結果です。
次回以降のレンダー時: Reactは各レンダーごとに、依存関係を直近に渡された依存値と比較します。依存関係に(Object.is()
による比較で)変わりなければ、useMemo
が返すのは直近に計算した値です。変更があったら、Reactは計算を再実行し、新たな結果の値を返します。
つまり、useMemo
は、依存関係が変わらないかぎり、再レンダリングの間計算結果をキャッシュするということです。
useMemo
がどう役立つのか、コード例で見ていきましょう。
デフォルトでは、Reactはコンポーネントが再レンダーされるたびに、本体全体を再実行します。たとえば、つぎのfilterTodos
関数は、TodoList
コンポーネントの状態が更新されるか、親から新たなプロパティを受け取ると再実行されます。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const visibleTodos = filterTodos(todos, tab);
}
通常、ほとんどの計算はきわめて高速なので、問題にはなりません。けれど、大きな配列をフィルタリングしたり、変換するときや、実行するのが負荷の高い計算になってくると別です。データが変わっていないなら、再実行は省きたいでしょう。todos
とtab
がともに直近のレンダー時と同じだった場合、前掲コード例のようにuseMemo
で計算を包んであれば再実行はされません。すでに計算済みのvisibleTodos
の値が再利用されるからです。
このようなキャッシュの仕方をメモ化と呼びます。
useMemo
は、もっぱらパフォーマンスを最適化するためにお使いください。このフックを使わないとコードが正しく動かないという場合、原因はフックではありません。根本的な問題を探り、修正することが先です。useMemo
はそのうえで、パフォーマンスを向上させるために加えましょう。
計算の負荷の高さをどう確かめるか
一般的には、数千ものオブジェクトを生成したり、ループ処理でもしないかぎり、おそらく高い負荷にはなりません。もっと具体的に確かめたい場合には、コンソールへのログを加えるとよいでしょう。特定のコードに費やされた時間が測れます。
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
測定するインタラクションを実行してみましょう(たとえば、テキストフィールドへの入力など)。前掲コード例なら、コンソールにfilter array: 0.15ms
といったログが出力されるはずです。ログに記録された時間の合計値がかなり大きな場合(1ミリ秒以上くらい)、メモ化することを考えてよいかもしれません。試しに、その計算をuseMemo
で包んでみます。そのうえで、インタラクションによりログに記録される合計時間が減るかどうか確かめてください。
console.time('filter array');
// 依存値todosとtabに変更がなければ再計算が省かれる
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.timeEnd('filter array');
useMemo
を用いても初期レンダーは高速化されません。必要のない処理を更新時に省くために役立つだけです。
開発用のマシンは、おそらくユーザーよりも高速であることにご注意ください。ですから、パフォーマンスのテストは、人為的に速度を落として行うのがよいでしょう。Chromeであれば、モバイルデバイスをシミュレートする機能が備わっています(「モバイルデバイスのシミュレート」参考)。
また、開発環境でパフォーマンスを測っても、厳密な結果は得られないことにもご注意ください(たとえば、開発環境でStrictMode
が有効なとき、各コンポーネントは、初回に1度ではなく2度レンダーされて表示されます)。もっとも正確な結果を得るには、実稼働用に構築したアプリケーションで、ユーザーが使っているのと同じデバイスによりテストすることです。
useMemo
はどこに使うか
アプリケーションがこの技術ドキュメントのようなサイトで、ほとんどのインタラクションは大雑把(ページやセクション全体を書き替えるなど)な場合、通常メモ化は要らないでしょう。それに対して、描画エディタのように操作の粒度が(図形の移動など)細かいアプリケーションなら、メモ化は役立つかもしれなません。
useMemo
による最適化が有効なのは、つぎのような場合にかぎられます。
-
useMemo
で実行している計算がきわめて遅く、依存関係はほとんど変化ないときです。 - 計算は
memo
に包んで、コンポーネントにプロパティとして渡せます。再レンダリングするのは、プロパティ値が変わっていなかったら避けたいでしょう。メモ化すれば、コンポーネントの再レンダーは、依存関係が同じでないときにだけ実行されるのです。 - コンポーネントに渡した値は、そのあとフックの依存関係に用いられます。たとえば、別の
useMemo
の計算値が依存するかもしれません。あるいは、useEffect
の依存値になることもあるでしょう。
計算をuseMemo
で包むことが役立つのは、上記以外にはありません。とはいえ、ことさら害もないでしょう。そのため、チームによっては、具体的な場合について個別に考えることなく、できるかぎりメモ化することを選ぶかもしれません。それで問題となるのは、コードが読みにくくなることです。また、メモ化の効果がないこともありえます。たとえば、「つねに更新される」値がひとつ含まれるだけで、コンポーネント全体のメモ化は無意味になるでしょう。
実際には、いくつかの原則にしたがうことで、多くのメモ化は要らなくなります。
- コンポーネントが表示上子を包む場合は、JSXを
children
で与えることです。そうすると、ラッパーコンポーネントが自らの状態を更新するとき、Reactは子の再レンダーは要らないと認識できます。 - 状態はできるだけローカルにもたせましょう。必要がないかぎり、状態を引き上げることは避けてください。たとえば、フォームのような一時的な状態を親に保持すべきではありません。あるいは、ツリーのトップで要素にポインタが重なっているかとか、グローバルな状態ライブラリについても同様です。
- レンダリングロジックは純粋に保ちましょう。コンポーネントを再レンダリングすると問題が生じたり、表示の明らかな不具合は、コンポーネントのバグです。メモ化より、バグを直しましょう。
- 状態を更新する要らないエフェクトは避けてください。Reactアプリケーションでパフォーマンスが問題となる多くの場合、原因は更新の連鎖です。その発端はエフェクトで、レンダーが繰り返されることとなります。
- エフェクトからは、不要な依存関係は除きましょう。たとえば、多くの場合メモ化するより、オブジェクトや関数をエフェクト内に含めたり、コンポーネントの外に移す方が端的です。
特定のインタラクションがまだ遅いと感じる場合は、React Developer Tools profilerをお使いください。どのコンポーネントにメモ化の効果が高いかを確かめられます。そのうえで、必要なコードにメモ化を加えるべきです。これらの原則により、コンポーネントのデバッグと理解がしやすくなります。したがって、どのような場合であっても、これらの原則にはしたがうのがよいでしょう。将来的に、きめ細かなメモ化を自動的に行う研究を進めています。実装された際には、問題は完全に解決されるはずです。
useMemo
と直接値を計算することの違い
useMemo
を使ったときと、直性計算した場合の違いを、コードサンプルで確かめましょう。Todoリストの処理済みと未処理の項目をフィルタリングする例です。50のTodo項目から、表示をタブ(ボタン)で切り替えます。フィルタリングする関数filterTodos
の実装は、処理を人為的に遅くしました。
export const filterTodos = (todos: Todo[], tab: string) => {
Array.from(new Array(10000000), (_, i) => i); // 人為的に速度を低下
return todos.filter((todo: Todo) => {
switch (tab) {
case 'all':
return true;
case 'active':
return !todo.completed;
case 'completed':
return todo.completed;
default:
return null;
}
});
};
useMemo
で再計算を省く例
以下のサンプル001は、useMemo
で再計算を省いた例です。タブ(tab
)の切り替えによるTodo項目リストのレンダリングは遅くなっています。けれど、テーマ(theme
)を変更したときの表示は速いでしょう。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
return (
<div className={theme}>
</div>
);
};
タブを切り替えたとき、filterTodos
は再実行されます(2度実行される理由については後述「再レンダリングのたびに計算が2回実行される」)。TodoList
コンポーネントに定めたuseMemo
の依存配列にtab
が加わっているからです。これに対して、テーマ(theme
)は依存関係に含まれません。そのため、テーマを変えた場合は、直近のレンダー時の値(visibleTodos
)が用いられ、関数filterTodos
の呼び出しは省かれるのです。
サンプル001■React + TypeScript: useMemo 01
つねに値を再計算する例
前掲サンプル001からuseMemo
を外したのが、以下のサンプル002です。タブ(tab
)を切り替えたときだけでなく、テーマ(theme
)の変更による表示も遅くなってしまいました。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
console.time("filter array");
// const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const visibleTodos = filterTodos(todos, tab);
};
useMemo
に包まれない関数filterTodos
が、再レンダーのたびに呼び出されるからです。theme
を変更しても、tab
(およびtodos
)の値が同じなら、関数filterTodos
の戻り値は直近のレンダー時と変わりません。それでも、useMemo
を用いなければ、関数の再計算が省かれないのです。
サンプル002■React + TypeScript: useMemo 02
ただ、関数filterTodos
の処理を人為的に遅くしたコードは除いてみてください。useMemo
がなくても、気にならないのではないでしょうか。
export const filterTodos = (todos: Todo[], tab: string) => {
// Array.from(new Array(10000000), (_, i) => i); // 人為的に速度を低下
return todos.filter((todo: Todo) => {
});
};
多くの場合、メモ化しなくても、コードは正しく動きます。インタラクションが十分に速いなら、メモ化は要らないでしょう。
前掲サンプル002で処理を人為的に遅くしなくても、Todo項目数(todos.length
)が大幅に増えれば負荷は高まります。
export const createTodos = () => {
// const todos = Array.from(new Array(50), (_, i) => ({
const todos = Array.from(new Array(50000), (_, i) => ({
}));
return todos;
};
もっとも、負荷が生じる原因のほとんどは、フィルタリングでなく再レンダリングです。そこで、useMemo
を用いて再レンダリングがどう最適化できるかご説明しましょう。
コンポーネントの再レンダリングを省く
useMemo
は、子コンポーネントを再レンダリングするパフォーマンスの最適化に役立つこともあります。具体的な例で確かめるため、前掲サンプル002のコードを書き替えてみましょう。TodoList
コンポーネントから新たに子としてList
を切り出します。渡すプロパティitems
の値は、フィルタリングしたTodo項目リストのvisibleTodos
です。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
return (
<div className={theme}>
{/* <ul>
</ul> */}
<List items={visibleTodos} />
</div>
);
};
export const List: FC<Props> = ({ items }) => {
return (
<ul>
{items.map((todo) => (
<li key={todo.id}>
{todo.completed ? <s>{todo.text}</s> : todo.text}
</li>
))}
</ul>
);
};
デフォルトでは、コンポーネントが再レンダーされると、Reactはその子コンポーネントすべてを再帰的に再レンダーします。つまり、TodoList
のプロパティtheme
が変わって再レンダーされると、子のList
コンポーネントもたとえプロパティitems
の値は前と同じでも再レンダーされるのです。前掲コード例のList
コンポーネントには重い処理は含まれないので、それでもとくに問題ありません。けれど、再レンダーが遅いと確認されたら、memo
を活用するとよいでしょう。コンポーネントをmemo
でラップすると、プロパティが直近のレンダー時と同じだったら、再レンダリングされません。
今回は、List
がレンダリング負荷の高いコンポーネントだとしましょう(後掲サンプル003には人為的な負荷を加えました)。関数コンポーネント全体をmemo
で包んでください。
import { memo } from 'react';
// export const List: FC<Props> = ({ items }) => {
export const List: FC<Props> = memo(({ items }) => {
// };
});
memo
にラップしたので、List
のすべてのプロパティ(型Props
)が直近のレンダー時と同じであれば、コンポーネントの再レンダリングは省かれます。ただし、ここで計算のキャッシュに注意しなければなりません。書き替えもとのサンプル002では、visibleTodos
を得るための関数filterTodos
の呼び出しにuseMemo
は用いませんでした。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
};
すると、filterTodos
関数は、たとえ直近のレンダー時と引数(todos
とtab
)の値が同じだとしても、つねに異なる新たな配列をつくって返すのです(オブジェクトリテラルの作成でも同様)。つまり、List
コンポーネントのプロパティitems
の値visibleTodos
が再レンダー間で同じになることはありません。memo
によるList
の最適化の効果は失われてしまいます。このとき役に立つのがuseMemo
です。
visibleTodos
の計算をuseMemo
で包めば、依存関係にまったく変わりないなら、再レンダー間で必ず同じ値が返されます(サンプル003)。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
// const visibleTodos = filterTodos(todos, tab);
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
};
特別な理由がなければ、useMemo
で計算をラップする必要はありません。このコード例におけるuseMemo
の使用は、memo
で包んだList
コンポーネントに渡すプロパティitems
の値(visibleTodos
)が直近と同じとき、再レンダリングを省くためです。useMemo
を加える他の理由については、このあとご説明しましょう。
サンプル003■React + TypeScript: useMemo 03
個々のJSXノードのメモ化
List
コンポーネントはmemo
で包まずに、JSXノード<List />
をuseMemo
でラップしても構いません(サンプル004)。
// export const List: FC<Props> = memo(({ items }) => {
export const List: FC<Props> = ({ items }) => {
// });
};
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{/* <List items={visibleTodos} /> */}
{children}
</div>
);
};
動きは前掲003と同じです。メモ化されたvisibleTodos
の値が直近のレンダー時と変わらなければ、List
コンポーネントは再レンダリングされません。
サンプル004■React + TypeScript: useMemo 04
<List items={visibleTodos} />
といったJSXノードは、{ type: List, props: { items: visibleTodos } }
のようなオブジェクトです。このオブジェクトをつくる負荷はきわめて低いといえます。しかしReactは、その中身が前回と同じかどうかはわかりません。そのため、デフォルトではReactはList
コンポーネントを再レンダーするのです。
けれども、Reactが直近のレンダーとまったく同じJSXだと認識できれば、コンポーネントは再レンダリングされません。JSXノードはイミュータブルだからです。JSXノードは、つくられたあとに変更されることがありません。したがって、同じJSXであれば、Reactは再レンダーを省いても問題ないと認識します。ただし、これができるためには、ノードが実際に同じオブジェクトでなければなりません。コードの記述が同じだけでは足りないのです。前掲コード例では、そのためにuseMemo
を用いました。
とはいえ、JSXノードをいちいちuseMemo
で包むのも不便です。また、フックですので、条件づきでは使えません。通常、コンポーネントをmemo
で包み、JSXノードのラップが使われないのはこのためです。
再レンダリングを省くかつねに実行するかの違い
前掲サンプル003では、useMemo
とmemo
の組み合わせで、List
コンポーネントの再レンダリングを最適化しました。タブ(tab
)を切り替えると、TodoList
に定められたuseMemo
の依存関係が変わりますので、visibleTodos
は再計算され、List
コンポーネントも再レンダーされます。けれど、テーマ(theme
)が変わっても、useMemo
の依存関係には含まれないのでvisibleTodos
の値は直近のままです。そのため、List
コンポーネントは再レンダリングされません。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
};
export const List: FC<Props> = memo(({ items }) => {
});
ここで、TodoList
コンポーネントでvisibleTodos
の計算に用いたuseMemo
を外してみましょう。filterTodos
関数は直近のレンダー時と引数の値が変わらなくても、異なる新たな配列をつくって変数visibleTodos
の値として返します。すると、List
コンポーネントはつねに再レンダリングされるということです。つまり、List
をmemo
で包んだ意味はもはやありません。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
// const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const visibleTodos = filterTodos(todos, tab);
};
つぎにさらに、List
に加えてあった人為的に速度を落とすコードも除いてみます。useMemo
を使っていなくても、ほとんど気にならないでしょう。
多くの場合、コードはメモ化しなくても動きに問題は生じません。インタラクションが十分に速ければ、メモ化しなくてもよいのです。
アプリケーションが遅くなっている原因を現実的につかむには、つぎの点について心がけてください。
- Reactは本番モードで実行します。
- React Developer Toolsは無効にしましょう。
- アプリケーションのユーザーがもっているものと同様のデバイスを使ってください。
他のフックの依存関係をメモ化する
コンポーネント本体で直接つくられたオブジェクトに計算が依存するとしましょう。
const Dropdown = ({ allItems, text }: Props) => {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 注意: 依存するオブジェクトがコンポーネント本体で生成
}
関数本体で生成したオブジェクトに依存する関数は、メモ化しても意味がありません。コンポーネントが再レンダーされると、関数本体に直接書かれたコードはすべて再実行されます。searchOptions
オブジェクトをつくるコードも、再レンダーのたびに実行されるのです。searchOptions
はuseMemo
を呼び出す依存関係に含まれます。searchOptions
の値は、毎回けっして同じにはなりません。Reactは、依存関係が異なると認識し、毎回searchItems
は再計算されるのです。
解決するには、searchOptions
オブジェクト自体もメモ化します。そのうえで、依存関係として渡せばよいでしょう。これで、プロパティtext
の値が直近のレンダー時と同じであれば、searchOptions
オブジェクトはそのまま変わりません。
const Dropdown = ({ allItems, text }: Props) => {
// const searchOptions = { matchMode: 'whole-word', text };
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ OK: textが変わったときのみ更新
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ OK: allItemsかsearchOptionsが変わったときのみ更新
}
もっとも、searchOptions
をメモ化していなかったとき、リンターから警告としてもうひとつの対処法が提案されていました。searchOptions
オブジェクトをuseMemo
の計算関数(コールバック)内に移すことです。
The 'searchOptions' object makes the dependencies of useMemo Hook change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'searchOptions' in its own useMemo() Hook
これで、useMemo
の依存関係には、text
がsearchOptions
オブジェクトに替わって直接加わりました。text
は文字列ですので、コンポーネントのレンダリングだけで値が更新されることはありません。
const Dropdown = ({ allItems, text }: Props) => {
/* const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); */
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
// }, [allItems, searchOptions]);
}, [allItems, text]); // ✅ OK: allItemsかtextが変わったときのみ更新
}
関数をメモ化する
つぎのコード例で、Form
コンポーネントはメモ化されているとします。渡すプロパティは関数handleSubmit
です。
export const Page: FC<Props> = ({ productId, referrer }) => {
const handleSubmit = (orderDetails: OrderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
};
コンポーネントに定められた関数(関数宣言function() {}
もしくは関数式() => {}
)は、再レンダリングのたびに新たな異なる関数としてつくられます。オブジェクトリテラル{}
の生成と同じです。関数が新しく作成されることそのものは問題ありません。けれど、今回はForm
コンポーネントがメモ化されていると想定しました。つまり、プロパティの値が同じなら、再レンダリングは省きたいということです。プロパティがつねに異なって扱われるのでは、メモ化した意味がありません。
useMemo
を用いて関数をメモ化しましょう。計算関数の戻り値は、メモ化する関数です。
export const Page: FC<Props> = ({ productId, referrer }) => {
// const handleSubmit = (orderDetails: OrderDetails) => {
const handleSubmit = useMemo(() => {
return (orderDetails: OrderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
}, [productId, referrer]);
};
これで、関数はメモ化できました。けれど、関数が入れ子になって、見づらく不便です。関数のメモ化はよく使われるので、Reactには専用のフックが備わっています。メモ化する関数は、useMemo
でなくuseCallback
で包んでください。余分な関数の入れ子がなくなります。
export const Page: FC<Props> = ({ productId, referrer }) => {
// const handleSubmit = useMemo(() => {
const handleSubmit = useCallback((orderDetails: OrderDetails) => {
// return (orderDetails: OrderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
// }}
}
, [productId, referrer]);
};
useMemo
でもuseCallback
でも、結果は変わりません。useCallback
を使えば、フックに書く関数の入れ子が減らせるというだけです。ほかに違いはありません。詳しくは「useCallback」をお読みください。
トラブルへの対応
再レンダリングのたびに計算が2回実行される
StrictMode
では、Reactは一部の関数を1度だけではなく2度呼び出します。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
// つぎのコンポーネント関数はレンダーごとに2度呼び出される
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
};
この動作は開発専用で、コードは破綻させません。コンポーネントを純粋に保つための機能です。Reactは、呼び出しの一方の結果を用い、もう一方は無視します。コンポーネントと計算関数が純粋であるかぎり、ロジックには影響しません。けれど、誤って純粋でなくなった場合に、気づいて修正するのに役立つのです。
たとえば、つぎの計算関数は純粋でなく、プロパティとして受け取った配列を直接書き替えています。
const visibleTodos = useMemo(() => {
// 🚩 NG: プロパティを直接変更
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);
開発環境ではReactは関数を2度呼び出すので、todos
にオブジェクトが2回加えられることに気づくでしょう。計算のコード内では、既存のオブジェクトを書き替えることは禁じられます。けれど、計算中につくった新たなオブジェクトは変更しても構いません。たとえば、todos
の複製にオブジェクトを加えれば、その新しい配列で処理が進められます。todos
は変わっていないので、関数が2回呼ばれても問題ありません。
const visibleTodos = useMemo(() => {
// ✅ OK: todosの複製にオブジェクトを追加
const newTodos = [...todos, { id: 'last', text: 'Go for a walk!' }];
const filtered = filterTodos(newTodos, tab);
return filtered;
}, [todos, tab]);
詳しくは、以下をご参照ください。
- 「コンポーネントを純粋に保つ(Keeping Components Pure)」
- 「React + TypeScript: 状態に収めたオブジェクトを更新する」
- 「React + TypeScript: 状態に収めた配列を更新する」
オブジェクトを返すはずのuseMemo
の呼び出しで戻り値がundefined
になる
正しく動かないコード例がつぎです。
// 🔴 NG: () => {}の構文ではオブジェクトは返されない
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
誤りはReactでなく、JavaScriptのアロー関数式の構文にあります。() => {}
の記述では、{}
がコードブロックとみなされ、その中身は関数本体と解釈されるのです。したがって、値を返すにはreturn
が必要となります。
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
もっと簡単なのは、{}
をさらに()
で囲むことです。これで、コードブロックとはみなされません。
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
ただし、()
の必要性が理解されなかったり、見逃されることもありがちです。それも考え合わせたうえで、明示的にreturn
を加えるか判断するのがよいでしょう。
コンポーネントのレンダーのたびにuseMemo
の計算が再実行される
コンポーネントのレンダーのたびにuseMemo
の計算が再実行される場合、まず第2引数の依存関係を確かめてください。依存関係の配列が省かれていると、useMemo
は毎レンダー時再実行されます。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
// 🔴 NG: 依存関係の配列がないと毎レンダー時再計算される
const visibleTodos = useMemo(() => filterTodos(todos, tab));
};
第2引数には、正しく依存関係の配列を与えてください。
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
// ✅ OK: 依存関係が変わらなければ再計算されない
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
};
依存配列を加えても解決しない場合には、少なくともひとつの依存値が直近のレンダー時とは異なっているということです。問題は個別にデバッグして調べるしかありません。依存値をつぎのようにコンソールに出力しましょう。
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);
そのうえで、コンソールに示される再レンダーごとの連続する配列をそれぞれ右クリックして、[objectをグローバル変数として保存]します(「グローバル変数として格納」参照)。すると、各配列はグローバル変数になり、temp1
、temp2
といった連番の名前がつけられるのです。グローバル変数として保存した配列は、ブラウザコンソール内でJavaScriptにより操作できます。そこで、依存値がレンダーごとに同じかどうか調べればよいでしょう。
Object.is(temp1[0], temp2[0]); // ひとつめの依存値が同じかどうか
Object.is(temp1[1], temp2[1]); // ふたつめの依存値が同じかどうか
メモ化の妨げとなっている依存値が見つかったら、依存関係から除く方法を考えるか、その値もメモ化してください。
ループ処理内で各リスト項目ごとにuseMemo
を呼び出すことが許されない
つぎのコード例のChart
コンポーネントは、memo
でラップされているとします。すると、ReportList
が再レンダーされるとき、リスト表示されるChart
の再レンダリングは、プロパティ(data
)が変わっていないかぎり省きたいでしょう。しかし、ループ処理の中でuseMemo
を呼び出すことは許されません。
const ReportList: FC<Props> = ({ items }) => {
return (
<article>
{items.map(item => {
// 🔴 NG: ループ処理内でuseMemoは呼び出せない
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
};
この場合、リストの各項目となるコンポーネントを切り出し、与えるデータはそれぞれ個別にメモ化してください。
const Report: FC<PropsReport> = ({ item }) => {
// ✅ OK: useMemoはトップレベルで呼び出す
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
};
const ReportList: FC<Props> = ({ items }) => {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
};
このコード例の場合、useMemo
を除いてしまうこともできます。Report
コンポーネントそのものをmemo
で包むのです。受け取ったプロパティ(item
)が直近のレンダー時と変わらなければ、Report
は再レンダリングされません。したがって、Chart
の再レンダリングも省かれるのです。
const Report: FC<Props> = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});