どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。
ブログアシスタントのふーちゃんです。
React は JavaScript を扱うライブラリなので、コンポーネントを構成するために、関数で計算をおこなっています。
純粋関数の計算であれば、引数が同じなら、必ず同じ返り値を得られます。この返り値をメモしておければ、同じ引数が渡された時にメモ化した返り値が使えるので、大幅に計算処理を削減できます。
この記事では、 React で関数の返り値をメモ化できる useMemo フックについて説明します。
React の useMemo フックとは
React の useMemo フックは、純粋関数の返り値をメモ化(保持)できる機能です。
メモ化とは
何かしらの処理を実行した時に、実行によって得られた値を保持しておき、再利用する手法。あとで同じ処理が必要になった時に、保持しておいた値を使い回せるので、処理を高速化できる。
純粋関数の計算は、同じ引数を渡せば、必ず同じ返り値を得られます。そのため、『純粋関数に同じ引数が渡されるたびに、同じ計算を繰り返して、同じ返り値を得る』という工程は、とても非効率です。
もしも、『引数と返り値』のセットをメモしておければ、純粋関数に同じ引数が渡された時に、メモ化した返り値を使い回せるようになります。わざわざ計算する必要がありません。
React は SPA (シングル・ページ・アプリケーション)なので、頻繁に再レンダリングされます。もちろん、再びレンダリングのたびに、コンポーネントを構成する大量の関数を実行して、差分を検知しています。
useMemo フックを使えば、関数による計算処理を削減できるので、ブラウザのパフォーマンス向上が期待できます。
React は、とあるコンポーネントの state や props などに更新があった時に、『そのコンポーネントにぶら下がっている全ての子コンポーネント』も再レンダリングの対象にします。
マウントされる部分が少なくても、その背景では、膨大なレンダリングがおこなわれているわけです。
フック呼び出しのオーバーヘッドを考慮しつつ、 useMemo フックを使って、純粋関数をメモ化していきましょう。
複雑な計算が必要な純粋関数(高価な関数)ほど、 useMemo フックが真価を発揮してくれます。
React の useMemo フックの基本構文
React の useMemo フックを使う時は、 useMemo の第 1 引数として、純粋関数を設定します。つまり、純粋関数をコールバック関数として扱います。また、 useMemo フックの第 2 引数には、『更新を監視したい変数を要素として持つ依存配列』を設定します。
こちらは、 JavaScript で useMemo を書いた場合のコードです。
const memoizedValue = useMemo(() => computeExpensiveFunction(a, b), [a, b]);
こちらは、 TypeScript で useMemo を書いた場合のコードです。 useMemo の直後に、返り値を型宣言します。
const memoizedValue = useMemo<Type>(() => computeExpensiveFunction(a, b), [a, b]);
useMemo を使う時には、はじめに「 react 」から「 useMemo 」を import してください。
import React, { useMemo } from "react";
useMemo フックの依存配列が要素を持っている場合
React の useMemo の依存配列が要素として変数を持っている場合は、その変数は監視対象になります。変数に更新があった時には、あらためて純粋関数が実行されて、新しく得られた返り値がメモ化されます。
useMemo の挙動が分かりやすいように、 2 つの計算機を用意しました。
一方は、入力欄の数値を 2 倍にして表示します。もう一方は、入力欄の数値を 3 倍にして表示します。
入力欄の数字を変えると関数が実行されて、ページの倍数値を表示しつつ、コンソールにも出力します。
たとえば、 2 倍用の入力欄の数値を変えても、 3 倍用の関数が実行されていない( console.log が実行されない)ことに注目してください。
3 倍用の関数の依存配列の変数( inputTripel )が更新されていないので、メモ化が機能して、レンダリングされなかった証拠ですよ。
import React, { useState, useMemo } from "react";
export const Counter: React.VFC = () => {
const [inputDouble, setInputDouble] = useState<number>(5);
const [inputTripel, setInputTripel] = useState<number>(5);
const CalculateDouble = (num: number) => {
let double = num * 2;
console.log("double: " + double);
return double;
};
const CalculateTriple = (num: number) => {
let triple = num * 3;
console.log("triple: " + triple);
return triple;
};
// useMemoの依存配列の要素にdoubleNumを設定
const doubleNum = useMemo<number>(() => CalculateDouble(inputDouble), [
inputDouble
]);
// useMemoの依存配列の要素にtripleNumを設定
const tripleNum = useMemo<number>(() => CalculateTriple(inputTripel), [
inputTripel
]);
return (
<>
<div style={{ marginBottom: "40px" }}>
<p>入力値の2倍: {doubleNum}</p>
<input
type="number"
defaultValue="5"
onChange={(e) => setInputDouble(Number(e.target.value))}
/>
</div>
<div>
<p>入力値の3倍: {tripleNum}</p>
<input
type="number"
defaultValue="5"
onChange={(e) => setInputTripel(Number(e.target.value))}
/>
</div>
</>
);
};
useMemo フックの依存配列が空になっている場合
React の useMemo の依存配列を空にした場合は、 useMemo 内の関数をはじめて実行した時のみ、メモ化されます。あとから関数の内部で使っている変数が更新されて、返り値も更新されるべき場合でも、初回実行時にメモ化した返り値が再使用されます。
useMemo の依存配列を空にした場合です。
最初にページが表示された時点で、計算結果がメモ化されます。入力値を変更しても、常にメモ化した返り値が使われます。
どちらの入力欄の数値を変えても、ページに表示されている倍数値が変わりません。
また、コンソールログの出力も、はじめてページ表示された時の内容のみです。
メモ化が機能し続けて、再レンダリングされない証拠ですね。
import React, { useState, useMemo } from "react";
export const Counter: React.VFC = () => {
const [inputDouble, setInputDouble] = useState<number>(5);
const [inputTripel, setInputTripel] = useState<number>(5);
const CalculateDouble = (num: number) => {
let double = num * 2;
console.log("double: " + double);
return double;
};
const CalculateTriple = (num: number) => {
let triple = num * 3;
console.log("triple: " + triple);
return triple;
};
// useMemoの依存配列を空にする
const doubleNum = useMemo<number>(() => CalculateDouble(inputDouble), []);
// useMemoの依存配列を空にする
const tripleNum = useMemo<number>(() => CalculateTriple(inputTripel), []);
return (
<>
<div style={{ marginBottom: "40px" }}>
<p>入力値の2倍: {doubleNum}</p>
<input
type="number"
defaultValue="5"
onChange={(e) => setInputDouble(Number(e.target.value))}
/>
</div>
<div>
<p>入力値の3倍: {tripleNum}</p>
<input
type="number"
defaultValue="5"
onChange={(e) => setInputTripel(Number(e.target.value))}
/>
</div>
</>
);
};
React.memo ・ useCallback ・ useMemo の違い
React のメモ化に関する機能として、 React.memo ・ useCallback ・ useMemo の 3 つが挙げられます。それぞれの機能を簡潔にまとめると、以下のような違いがあります。
- React.memo
- コンポーネントそのものをメモ化できる。コンポーネントに渡される props を監視しており、 props が同じ場合には、メモ化したコンポーネントを使い回す。コンポーネントに対する不要なレンダリングを省略できる。
- useCallback フック
- コールバック関数をメモ化できる。依存配列の要素として設定した変数を監視しており、変数が同じ場合には、メモ化した関数を使い回す。関数を再生成する工程を省略できる。基本的には、 React.memo と併用する。
- useMemo フック
- 純粋関数の返り値をメモ化できる。依存配列の要素として設定した変数を監視しており、変数が同じ場合には、メモ化した返り値を使い回す。純粋関数に対する不要な計算を省略できる。
React.memo ・ useCallback ・ useMemo の違いついての参考記事