React.memo, useCallBack, useMemoを使う理由
パフォーマンス改善のために使う。です。
React.memo, useCallback, useMemoをちゃんと使うとメモ化
され無駄なレンダリングが抑制されます。
メモ化とは、以下です。
- 再レンダリングされても同じ結果になるものは、初回のものを記録する。
- 値を記録し再レンダリングされても値を計算しない。 ※値が変化するときのみ処理する。
よく分からないですね。
後半のサンプル動かしながら覚えていきましょう。
Reactをやり始めの頃は、ただただ関数にuseCallbackを付けたら、良いんだろ? と思い実装していました。
上記考え、やり方は無駄な実装です。
(厳密にいうと逆に、useCallbackを処理する分、パフォーマンス悪くなるとの意見もあります。)
ちゃんと理解して使用しましょうということです。
レンダリングが起こる条件
こことここがとても分かりやすかったです。特にここのサンプルを動かすと理解が深まると思います。
Reactコンポーネントの再レンダリングは、おおよそ以下3つの条件で発生します。
- propsの更新
- stateの更新(自コンポーネントのstate)
- 親コンポーネントが再レンダリングされた時
※一つでも条件に当てはまれば、再レンダリングされるということです。
素のサンプルコードを動かしてみよう
メモ化を施していない素のソースです。
import React, { useState } from "react";
/**
* 親コンポ
* @returns
*/
export const App: React.FC = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
//Aボタンのstateセット用関数
const incrementACounter = () => setCountStateA(countStateA + 1);
//Bボタンのstateセット用関数
const incrementBCounter = () => setCountStateB(countStateB + 1);
return (
<>
<Count text="A ボタン" countState={countStateA} />
<Count text="B ボタン" countState={countStateB} />
<button onClick={incrementACounter}>A ボタン</button>
<button onClick={incrementBCounter}>B ボタン</button>
</>
);
};
export default App;
/**
* 子コンポ
*/
const Count: React.FC<{ text: string; countState: any }> = ({
text,
countState,
}) => {
console.log("Count child component:", text);
return (
<p>
{text}:{countState}
</p>
);
};
Aボタン押下時に、Bボタンも描画されています。
Bボタン押下時に、Aボタンも描画されています。
React.memoを使用してみます。
先程のソースの子コンポをReact.memo
でラップします。
へい!よー!
import React, { useState } from "react";
/**
* 親コンポ
* @returns
*/
export const App: React.FC = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
//Aボタンのstateセット用関数
const incrementACounter = () => setCountStateA(countStateA + 1);
//Bボタンのstateセット用関数
const incrementBCounter = () => setCountStateB(countStateB + 1);
return (
<>
<Count text="A ボタン" countState={countStateA} />
<Count text="B ボタン" countState={countStateB} />
<button onClick={incrementACounter}>A ボタン</button>
<button onClick={incrementBCounter}>B ボタン</button>
</>
);
};
export default App;
/**
* 子コンポ
*/
// const Count: React.FC<{ text: string; countState: any }> = ({ ★ ここを変えました。
const Count: React.FC<{ text: string; countState: any }> = React.memo(
({ text, countState }) => {
console.log("Count child component:", text);
return (
<p>
{text}:{countState}
</p>
);
// }; ★ ここを変えました。
}
);
Aボタン押下時に、Bボタンは描画されなくなりました。
Bボタン押下時に、Aボタンは描画されなくなりました。
無駄な描画を抑制できました。
propsに関数がある場合
先程のReact.memoで無駄な描画を抑制できましたが、
原理を簡単に言うとpropsの中身が変わらなければ描画しない。
ということです。
しかし、propsが関数になると、オブジェクト?なので、毎度前回とは等価ではないとReactは判断するようです。
以下のソースを試してみましょう。
(前のソースを一部書き換えます。)
import React, { MouseEventHandler, useState } from "react";
/**
* 親コンポ
* @returns
*/
export const App: React.FC = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
//Aボタンのstateセット用関数
const incrementACounter = () => setCountStateA(countStateA + 1);
//Bボタンのstateセット用関数
const incrementBCounter = () => setCountStateB(countStateB + 1);
return (
<>
{/* ★ ここから書き換えます。 start */}
{/* <Count text="A ボタン" countState={countStateA} />
<Count text="B ボタン" countState={countStateB} />
<button onClick={incrementACounter}>A ボタン</button>
<button onClick={incrementBCounter}>B ボタン</button> */}
<p>A ボタン: {countStateA} </p>
<p>B ボタン: {countStateB} </p>
<Button counterState={incrementACounter} buttonValue="Aボタン" />
<Button counterState={incrementBCounter} buttonValue="Bボタン" />
{/* ★ end */}
</>
);
};
export default App;
// ★ 追加 start
/**
* 子コンポ(Button)※React.memoされています。
*/
const Button: React.FC<{
counterState: MouseEventHandler<HTMLButtonElement> | undefined;
buttonValue: string;
}> = React.memo(({ counterState, buttonValue }) => {
console.log("Button child component:", buttonValue);
return <button onClick={counterState}>{buttonValue}</button>;
});
// ★ 追加 end
// ★ 以下削除
// /**
// * 子コンポ
// */
// // const Count: React.FC<{ text: string; countState: any }> = ({ ★ ここを変えました。
// const Count: React.FC<{ text: string; countState: any }> = React.memo(
// ({ text, countState }) => {
// console.log("Count child component:", text);
// return (
// <p>
// {text}:{countState}
// </p>
// );
// // }; ★ ここを変えました。
// }
// );
また、
Aボタン押下時に、Bボタンも描画されています。
Bボタン押下時に、Aボタンも描画されています。
の状態になりました。
あかん。。
関数をuseCallbackでメモ化
してみます。
import React, { MouseEventHandler, useState, useCallback } from "react";
/**
* 親コンポ
* @returns
*/
export const App: React.FC = () => {
const [countStateA, setCountStateA] = useState(0);
const [countStateB, setCountStateB] = useState(0);
//Aボタンのstateセット用関数
// const incrementACounter = () => setCountStateA(countStateA + 1); //★ 書き換え
const incrementACounter = useCallback(
() => setCountStateA(countStateA + 1),
[countStateA]
);
//Bボタンのstateセット用関数
// const incrementBCounter = () => setCountStateB(countStateB + 1); // ★ 書き換え
const incrementBCounter = useCallback(
() => setCountStateB(countStateB + 1),
[countStateB]
);
return (
<>
<p>A ボタン: {countStateA} </p>
<p>B ボタン: {countStateB} </p>
<Button counterState={incrementACounter} buttonValue="Aボタン" />
<Button counterState={incrementBCounter} buttonValue="Bボタン" />
</>
);
};
export default App;
/**
* 子コンポ(Button)※React.memoされています。
*/
const Button: React.FC<{
counterState: MouseEventHandler<HTMLButtonElement> | undefined;
buttonValue: string;
}> = React.memo(({ counterState, buttonValue }) => {
console.log("Button child component:", buttonValue);
return <button onClick={counterState}>{buttonValue}</button>;
});
今度はちゃんと
Aボタン押下時に、Bボタンは描画されなくなりました。
Bボタン押下時に、Aボタンは描画されなくなりました。
に出来ました。
useMemoを使ってみる。
先程は、useCallback
を使って関数をメモ化
しました。
今度は、値自体をメモ化
します。
以下書きかけ