LoginSignup
33
32

React + TypeScript: useMemoフックの使い方と使いどころ

Last updated at Posted at 2023-09-07

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に包むことにより、再レンダーの間で計算をキャッシュします。フックはコンポーネントのトップレベルで呼び出してください。

src/TodoList.tsx
import { useMemo } from "react";

export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
	const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

};

useMemoに渡す引数はふたつです。

  1. 計算関数: 引数は取りません。計算した結果を返します。
    • () =>といった構文です。
  2. 依存関係のリスト: 計算で用いられるコンポーネント内のすべての依存値を配列で渡します。

初期レンダー時: useMemoから得られる値は、計算関数を呼び出した演算結果です。

次回以降のレンダー時: Reactは各レンダーごとに、依存関係を直近に渡された依存値と比較します。依存関係に(Object.is()による比較で)変わりなければ、useMemoが返すのは直近に計算した値です。変更があったら、Reactは計算を再実行し、新たな結果の値を返します。

つまり、useMemoは、依存関係が変わらないかぎり、再レンダリングの間計算結果をキャッシュするということです。

useMemoがどう役立つのか、コード例で見ていきましょう。

デフォルトでは、Reactはコンポーネントが再レンダーされるたびに、本体全体を再実行します。たとえば、つぎのfilterTodos関数は、TodoListコンポーネントの状態が更新されるか、親から新たなプロパティを受け取ると再実行されます。

export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
	const visibleTodos = filterTodos(todos, tab);

}

通常、ほとんどの計算はきわめて高速なので、問題にはなりません。けれど、大きな配列をフィルタリングしたり、変換するときや、実行するのが負荷の高い計算になってくると別です。データが変わっていないなら、再実行は省きたいでしょう。todostabがともに直近のレンダー時と同じだった場合、前掲コード例のように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で包むことが役立つのは、上記以外にはありません。とはいえ、ことさら害もないでしょう。そのため、チームによっては、具体的な場合について個別に考えることなく、できるかぎりメモ化することを選ぶかもしれません。それで問題となるのは、コードが読みにくくなることです。また、メモ化の効果がないこともありえます。たとえば、「つねに更新される」値がひとつ含まれるだけで、コンポーネント全体のメモ化は無意味になるでしょう。

実際には、いくつかの原則にしたがうことで、多くのメモ化は要らなくなります

  1. コンポーネントが表示上子を包む場合は、JSXをchildrenで与えることです。そうすると、ラッパーコンポーネントが自らの状態を更新するとき、Reactは子の再レンダーは要らないと認識できます。
  2. 状態はできるだけローカルにもたせましょう。必要がないかぎり、状態を引き上げることは避けてください。たとえば、フォームのような一時的な状態を親に保持すべきではありません。あるいは、ツリーのトップで要素にポインタが重なっているかとか、グローバルな状態ライブラリについても同様です。
  3. レンダリングロジックは純粋に保ちましょう。コンポーネントを再レンダリングすると問題が生じたり、表示の明らかな不具合は、コンポーネントのバグです。メモ化より、バグを直しましょう。
  4. 状態を更新する要らないエフェクトは避けてください。Reactアプリケーションでパフォーマンスが問題となる多くの場合、原因は更新の連鎖です。その発端はエフェクトで、レンダーが繰り返されることとなります。
  5. エフェクトからは、不要な依存関係は除きましょう。たとえば、多くの場合メモ化するより、オブジェクトや関数をエフェクト内に含めたり、コンポーネントの外に移す方が端的です。

特定のインタラクションがまだ遅いと感じる場合は、React Developer Tools profilerをお使いください。どのコンポーネントにメモ化の効果が高いかを確かめられます。そのうえで、必要なコードにメモ化を加えるべきです。これらの原則により、コンポーネントのデバッグと理解がしやすくなります。したがって、どのような場合であっても、これらの原則にはしたがうのがよいでしょう。将来的に、きめ細かなメモ化を自動的に行う研究を進めています。実装された際には、問題は完全に解決されるはずです。

useMemoと直接値を計算することの違い

useMemoを使ったときと、直性計算した場合の違いを、コードサンプルで確かめましょう。Todoリストの処理済みと未処理の項目をフィルタリングする例です。50のTodo項目から、表示をタブ(ボタン)で切り替えます。フィルタリングする関数filterTodosの実装は、処理を人為的に遅くしました

src/utils.ts
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)を変更したときの表示は速いでしょう。

src/TodoList.tsx
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)の変更による表示も遅くなってしまいました。

src/TodoList.tsx
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がなくても、気にならないのではないでしょうか。

src/utils.ts
export const filterTodos = (todos: Todo[], tab: string) => {
	// Array.from(new Array(10000000), (_, i) => i); // 人為的に速度を低下
	return todos.filter((todo: Todo) => {

	});
};

多くの場合、メモ化しなくても、コードは正しく動きます。インタラクションが十分に速いなら、メモ化は要らないでしょう。

前掲サンプル002で処理を人為的に遅くしなくても、Todo項目数(todos.length)が大幅に増えれば負荷は高まります。

src/utils.ts
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です。

src/TodoList.tsx
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {

	return (
		<div className={theme}>

			{/* <ul>

			</ul> */}
			<List items={visibleTodos} />
		</div>
	);
};
src/List.tsx
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で包んでください。

src/List.tsx
import { memo } from 'react';

// export const List: FC<Props> = ({ items }) => {
export const List: FC<Props> = memo(({ items }) => {

// };
});

memoにラップしたので、Listのすべてのプロパティ(型Props)が直近のレンダー時と同じであれば、コンポーネントの再レンダリングは省かれます。ただし、ここで計算のキャッシュに注意しなければなりません。書き替えもとのサンプル002では、visibleTodosを得るための関数filterTodosの呼び出しにuseMemoは用いませんでした。

src/TodoList.tsx
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
	const visibleTodos = filterTodos(todos, tab);

	return (
		<div className={theme}>

			<List items={visibleTodos} />
		</div>
	);
};

すると、filterTodos関数は、たとえ直近のレンダー時と引数(todostab)の値が同じだとしても、つねに異なる新たな配列をつくって返すのです(オブジェクトリテラルの作成でも同様)。つまり、Listコンポーネントのプロパティitemsの値visibleTodosが再レンダー間で同じになることはありません。memoによるListの最適化の効果は失われてしまいます。このとき役に立つのがuseMemoです。

visibleTodosの計算をuseMemoで包めば、依存関係にまったく変わりないなら、再レンダー間で必ず同じ値が返されます(サンプル003)。

src/TodoList.tsx
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)。

src/List.jsx
// export const List: FC<Props> = memo(({ items }) => {
export const List: FC<Props> = ({ items }) => {

// });
};
src/
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では、useMemomemoの組み合わせで、Listコンポーネントの再レンダリングを最適化しました。タブ(tab)を切り替えると、TodoListに定められたuseMemoの依存関係が変わりますので、visibleTodosは再計算され、Listコンポーネントも再レンダーされます。けれど、テーマ(theme)が変わっても、useMemoの依存関係には含まれないのでvisibleTodosの値は直近のままです。そのため、Listコンポーネントは再レンダリングされません。

src/TodoList.tsx
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
	const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
	return (
		<div className={theme}>

			<List items={visibleTodos} />
		</div>
	);
};
src/List.tsx
export const List: FC<Props> = memo(({ items }) => {

});

ここで、TodoListコンポーネントでvisibleTodosの計算に用いたuseMemoを外してみましょう。filterTodos関数は直近のレンダー時と引数の値が変わらなくても、異なる新たな配列をつくって変数visibleTodosの値として返します。すると、Listコンポーネントはつねに再レンダリングされるということです。つまり、Listmemoで包んだ意味はもはやありません。

src/TodoList.tsx
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オブジェクトをつくるコードも、再レンダーのたびに実行されるのですsearchOptionsuseMemoを呼び出す依存関係に含まれます。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の依存関係には、textsearchOptionsオブジェクトに替わって直接加わりました。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度呼び出します。

src/TodoList.tsx
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]);

詳しくは、以下をご参照ください。

オブジェクトを返すはずの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は毎レンダー時再実行されます。

src/TodoList.tsx
export const TodoList: FC<Props> = ({ todos, theme, tab }) => {
	// 🔴 NG: 依存関係の配列がないと毎レンダー時再計算される
	const visibleTodos = useMemo(() => filterTodos(todos, tab));

};

第2引数には、正しく依存関係の配列を与えてください。

src/TodoList.tsx
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をグローバル変数として保存]します(「グローバル変数として格納」参照)。すると、各配列はグローバル変数になり、temp1temp2といった連番の名前がつけられるのです。グローバル変数として保存した配列は、ブラウザコンソール内で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>
  );
});

33
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
32