ロードマップ
React 16.8 で追加された機能であるReactのHooks
について書いてあります。
書きながら学ぶ React Hooks 入門シリーズとして書き下ろしました。
はじめに
今回は、Reactの組み込みフックであるuseCallback
とuseMemo
の説明をします。
また、useCallback
は、React.memo
という Reactの API と併用するので、React.memo
の解説もします。
useCallback とは
一言で言うと、パフォーマンス向上のためのフックです。
具体的に言うと、コールバック関数をメモ化して、不要な関数インスタンスの作成を抑制します。
これによってパフォーマンスを向上させています(不要な関数インスタンスの作成が重い時は特に)。
ちなみに、メモ化とはプログラム高速化のための最適化の1つです。関数の定義や実行の結果を再利用するために一時的に保持しています。
依存配列の要素(deps)が変化した場合にのみメモ化した値を再計算します。
const callback = useCallback(コールバック関数, [依存配列])
依存している値がなければ、空の配列OKです。
const callback = useCallback(コールバック関数, [])
依存している値が更新されれば、関数が再生成されます。
例を1つ
const callback = useCallback(() => console.log(count), [count])
上記の場合、count
が更新されない限り、関数は再生成されません。
サンプルコード
こんなん作るとします。
各ボタンクリック時の結果は、なんとなくわかりますよね?
今の状態だと、どのボタンを押しても、Title
コンポーネントとか(再レンダリングしても結果同じなやつ)も含め、全てのコンポネントが再レンダリングされます。
console.log
仕込んでるので、それがわかります。
サンプルコード
import React, { useState } from "react";
import "./styles.css";
import Title from "./components/Title";
import Count from "./components/Count";
import Button from "./components/Button";
const App = () => {
const [height, setHeight] = useState(150);
const [weight, setWeight] = useState(50);
const incrementHeight = () => setHeight(height + 1);
const incrementWeight = () => setWeight(weight + 1);
return (
<>
<Title />
<Count text={"身長"} count={height} />
<Count text={"体重"} count={weight} />
<Button handleClick={incrementHeight}>身長+1</Button>
<Button handleClick={incrementWeight}>体重+1</Button>
</>
);
};
export default App;
import React from "react";
const Title = () => {
console.log("Title");
return <h1>身長と体重の入力</h1>;
};
export default Title;
import React from "react";
const Button = ({ handleClick, children }) => {
console.log(`Button - `, children);
return <button onClick={handleClick}>{children}</button>;
};
export default Button;
import React from "react";
const Count = ({ text, count }) => {
console.log(`Count - `, text);
return (
<p>
{text} : {count}
</p>
);
};
export default Count;
☝️のサンプルコードの問題点
どのボタンを押しても、全てのコンポネントが再レンダリングされることが、パフォーマンスが悪いとしましょう。
そこで、パフォーマンス改善に役立つReact.memo
とuseCallback
を試してみましょう。
そこで、React.memoの出番
React.memo とはコンポーネントをメモ化(計算結果を再利用するために保持)するReactのAPIです。
キャッシュみたいなもんです。
もう少し細かい言い方をすると、HOCで、React.memoを使うことで、propsの値が変わらないなら、関数コンポネントのレンダリングを抑制することができるAPIです。
なので、以下のようなコンポネントをメモ化するとパフォーマンス上有効です。
- レンダーコストが高いコンポネント
- 頻繁に再レンダーされるコンポネントの子コンポネント
やってみましょう。変更点は、赤枠のみ。👇
身長+1
ボタンをクリックしたら...
以下のコンポネントが再レンダリングされます。
- 身長のCountコンポネント
- 身長のButtonコンポネント
- 体重のButtonコンポネント(👈 なんでこいつが再レンダリングされるんだ? してほしくない)
以下のコンポネントは再レンダリングされません。
- 体重のCountコンポネント
- Titleのコンポネント
なぜ「身長+1ボタン」をクリックしたら体重のButtonコンポネントがレンダリングされるのか?
App コンポネントが再レンダリングされるたびに、関数も再生成され、再生成の前後で関数は等価ではありません。
なので、React.memo
で子コンポネントをメモ化しても コールバック関数をpropsとして渡す場合は子コンポコンポネントは必ず再レンダリングされます。
この問題を解決するのがuseCallback
です。
次に、usecallbackの出番
変更点は、赤枠。👇
実行結果
useCallbackを使うことでコンポネントの再レンダリングを最適化することができました。
このように、子コンポネントにコールバック関数をpropsとして渡す場合は使ってみるといいと思います。
useCallbackの注意点
前述の通り、useCallback
はReact.memo
と併用するものなので、次のような使い方をしても再レンダリングをスキップできません
-
React.memo
でメモ化していないコンポネントにuseCallback
でメモ化したコールバック関数を渡すとき -
useCallback
でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用するとき
useCallbackの疑問点...依存関係の配列にsetState関数を含める必要があるか?
ない。
コード書いてると見かけるので、調べてみましたが、公式ドキュメント見ると不要だそうです。
Note
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
訳
React は再レンダリングでsetState
関数の等価に保たれ、変化しないことを保証します。
従って useEffect
やuseCallback
の依存リスト(依存配列)にはsetState関数を含めなくてもいいです。
useMemo とは
useCallback
同様、パフォーマンス最適化用のHookです。
useCallback
との違いは以下です。
-
useCallback
は関数自体をメモ化 -
useMemo
は関数の結果をメモ化(メモ化された値を返すHook)
useMemo
は、値を算出するための不要な再計算をスキップすることでパフォーマンスを向上させます。
サンプルコード
こんなん作るとします。useCallback
の時とほとんど同じです。
身長だけ、奇数か偶数か判定されています。
差異があるファイルだけ以下に記載しておきます。
サンプルコード
import React, { useState, useCallback } from "react";
import "./styles.css";
import Count from "./components/Count";
import Button from "./components/Button";
const App = () => {
const [height, setHeight] = useState(150);
const [weight, setWeight] = useState(50);
const incrementHeight = useCallback(() => setHeight(height + 1), [height]);
const incrementWeight = useCallback(() => setWeight(weight + 1), [weight]);
const isEven = () => {
console.log("身長");
return height % 2 === 0;
};
return (
<>
<p>
身長 {height} は ({isEven() ? "偶数" : "奇数"})
</p>
<Count text={"身長"} count={height} />
<Count text={"体重"} count={weight} />
<Button handleClick={incrementHeight}>身長+1</Button>
<Button handleClick={incrementWeight}>体重+1</Button>
</>
);
};
export default App;
コードを実行すると、どちらのボタンをクリックしても、発火します。
理由は、useCallback
の時と同様に、コンポネントが再生成されたタイミングでisEven
関数も再作成、実行されるからです。
本来は、この関数は身長ボタンがクリックされたときだけ実行するべきですよね?
でも、体重ボタンをクリックしても発火してしまう。。
これを問題として、useMemo
を使って、身長ボタンがクリックされたときだけisEven
が実行されるようにしましょう。
サンプルコード
import React, { useState, useCallback, useMemo } from "react";
import "./styles.css";
import Count from "./components/Count";
import Button from "./components/Button";
const App = () => {
const [height, setHeight] = useState(150);
const [weight, setWeight] = useState(50);
const incrementHeight = useCallback(() => setHeight(height + 1), [height]);
const incrementWeight = useCallback(() => setWeight(weight + 1), [weight]);
const isEven = useMemo(() => {
console.log("身長");
return height % 2 === 0;
}, [height]);
return (
<>
<p>
身長 {height} は ({isEven ? "偶数" : "奇数"})
</p>
<Count text={"身長"} count={height} />
<Count text={"体重"} count={weight} />
<Button handleClick={incrementHeight}>身長+1</Button>
<Button handleClick={incrementWeight}>体重+1</Button>
</>
);
};
export default App;
変更点は、赤枠。👇
useMemo
は、値(今回は bool値)を算出するための不要な再計算(今回なら「体重+1」ボタンクリック時のisEven
内の計算処理)をスキップさせています。
「体重+1」ボタンクリック時は、useMemo
がisEven
処理を再実行させずに、結果(bool値)のみを返しています。
仮に、isEven
処理が重い場合、「体重+1」ボタンクリック時は、結果(bool値)のみを返すので、パフォーマンス向上につながると思います。
今回は以上です。
参考