React Hooksを基礎から理解する 5-3 useCallback React.memo useMemo
参考
-
useCallback()、useMemo()は、パフォーマンスチューニングのためのフック
-
コンポーネントの不要な再レンダリングを防ぐ
-
Reactコンポーネントでは、state、propsが更新されるたびに再レンダリングが行われる
-
useCallback()、React.memo()、useMemo()は、前回の差分がないことが事前に分かっている場合、再レンダリングをスキップできる
-
useCallback()は、React.memo()と組み合わせて使用する
-
メモ化とは
- 同じ結果を返す処理について、初回のみ処理を実行して記録
- 2回目以降は、保持していた結果を再利用する
React.memoとは
- コンポーネントのレンダリング結果をメモ化するReact API
- 親コンポーネントから渡されるpropsについて、変更差分をチェックして、コンポーネントが返した描画結果を記録してメモ化し、差分があった倍の三再レンダリングする
- propsの前後を比較し、等価かどうかチェック
- 等価であれば、メモ化したコンポーネントを利用するので、再レンダリングをスキップ
- 等価でなければ、再レンダリングする
- React.memo()でラップしていても、コンポーネント内部でuseState()やuseContext()を使用している場合、その変化に応じた再レンダリングが発生する
- useMemo()の基本構文
React.memo(親コンポーネントからpropsを受け取る子コンポーネント)
- React.memo()の利用例
- React.memo()未使用時(AボタンとBボタンを1回ずつ押す)
- React.memo()使用時(AボタンとBボタンを1回ずつ押す)
import React, { useState } from "react";
import "./styles.css";
// React.memo()で関数全体を括る。propsが同じときはメモ化したコンポーネントを使用する
const CountResult = React.memo(({ text, countState }) => {
console.log(`${text}ボタンがクリックされました!`);
return (
<p>
{text}: {countState}
</p>
);
});
const Counter = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
const countIncrementA = () => {
setCountStateA((prevCount) => prevCount + 1);
};
const countIncrementB = () => {
setCountStateB((prevCount) => prevCount + 1);
};
return (
<>
<CountResult text="Aボタン" countState={countStateA} />
<CountResult text="Bボタン" countState={countStateB} />
<button onClick={countIncrementA}>Aボタン</button>
<button onClick={countIncrementB}>Bボタン</button>
</>
);
};
export default function App() {
return <Counter />;
}
useCallbackとは
- React.memoを使っても親コンポーネントから、「コールバック関数」をpropsとして受け取った場合、子コンポーネントは再レンダリングされてしまう。
- 関数自体のメモ化に利用できるのが、メモ化されたコールバック関数を返すuseCallback()
- インポート
import React, { useCallback } from "react";
- useCallback()の基本構文
useCallback(コールバック関数, [コールバック関数が依存している要素の配列]);
- a,bどちらも変化しなければ、メモ化した関数を再利用する
// メモ化したいsampleCallbackFunc関数を宣言
const sampleCallbackFunc = () => doSomething(a, b);
// useCallbackでメモ化したい、sampleCallbackFunc関数をラップ
const memorizedSampleCallbackFunc = useCallback(sampleCallbackFunc, [a, b]);
- 配列を空にすると、初回レンダリング時に生成した関数をずっと使い続ける
const memorizedSampleCallbackFunc = useCallback(sampleCallbackFunc, []);
- useCallbackの利用例
- useCallback()は、React.memo()と組み合わせて使用する
- useCallback()で使用する配列の項目は、関数内で使われてないといけない。逆に使われていないときはその項目はいらない。
-
setCountStateA(countStateA + 1)
を、setCountStateA((prevCount) => prevCount + 1)
とすれば、[]
で良い
- Aボタン、Bボタンを1回ずつ押したとき、異なるボタンのログが出ていない。
import React, { useState, useCallback } from "react";
import "./styles.css";
// useCallback()は、React.memo()と組み合わせて使用する
const Button = React.memo(({ counterState, buttonValue }) => {
console.log(`${buttonValue}がクリックされました!`);
return <button onClick={counterState}>{buttonValue}</button>;
});
const Counter = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
// 関数をメモ化する
const countIncrementA = useCallback(() => {
setCountStateA(countStateA + 1);
}, [countStateA]);
// 関数をメモ化する
const countIncrementB = useCallback(() => {
setCountStateB(countStateB + 1);
}, [countStateB]);
return (
<>
<p>Aボタン: {countStateA}</p>
<p>Bボタン: {countStateB}</p>
<Button counterState={countIncrementA} buttonValue="Aボタン" />
<Button counterState={countIncrementB} buttonValue="Bボタン" />
</>
);
};
export default function App() {
return <Counter />;
}
useMemo
- 関数の結果を保持する
- 何度計算しても結果が同じ場合の、「関数による計算の結果」をメモ化し、そこから値を取得する
- useCallback() → 関数自体をメモ化する
- useMemo() → 関数の結果を保持する
- インポート
import React, { useMemo } from "react";
- useMemo()の基本構文
useMemo(() => 値を計算する関数の呼び出し, [値の計算に必要な要素の配列]);
- a,b のどちらかに変更があった場合、関数を再実行する。
useMemo(() => doSomething(a, b), [a, b]);
- 第二引数を空とした場合、初回の一度のみ実行され、それ以降はキャッシュから値を取得する
useMemo(() => doSomething(a, b), []);
- useMemoの利用例
- Bの計算がとても重いプログラム
- useMemo()を使用したとき(A,Bボタンを1回ずつクリック。重い処理が1度実行)
- useMemo()を使用しないとき(A,Bボタンを1回ずつクリック。重い処理が2度実行)
import React, { useState, useMemo } from "react";
import "./styles.css";
const square = (param) => {
const testData = [...Array(1000).keys()];
testData.forEach(() => {
console.log(
`「計算: B + 1」がボタンクリックされ、square関数実行、ループ処理を${testData.length}回実行中...`
);
});
return param * param;
};
const Counter = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
// squreがとても重い処理なので、countStateBが変化した時だけ実行してほしい
const squareArea = useMemo(() => square(countStateB), [countStateB]);
// ラップしないとき
// const squareArea = square(countStateB);
const countIncrementA = () => {
setCountStateA((prevCount) => prevCount + 1);
console.log("計算: A + 1ボタンがクリックされました!");
};
const countIncrementB = () => {
setCountStateB((prevCount) => prevCount + 1);
console.log("計算: B + 1ボタンがクリックされました!");
};
return (
<>
<p>
計算結果A: {countStateA}
<button onClick={countIncrementA}>計算: A + 1</button>
</p>
<p>
計算結果B: {countStateB}
<button onClick={countIncrementB}>計算: B + 1</button>
</p>
<p>【正方形の面積】</p>
<p>計算結果B × 計算結果B = {squareArea}</p>
</>
);
};
export default function App() {
return <Counter />;
}