16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React.memo / useCallback / useMemo を書きながら学ぶ(React Hooks 入門シリーズ 1/6)

Last updated at Posted at 2021-01-27

ロードマップ

React 16.8 で追加された機能であるReactのHooksについて書いてあります。

書きながら学ぶ React Hooks 入門シリーズとして書き下ろしました。

はじめに

今回は、Reactの組み込みフックであるuseCallbackuseMemoの説明をします。

また、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が更新されない限り、関数は再生成されません。

サンプルコード

こんなん作るとします。

スクリーンショット 2021-01-28 0.17.11.png

各ボタンクリック時の結果は、なんとなくわかりますよね?

スクリーンショット 2021-01-28 0.49.04.png

今の状態だと、どのボタンを押しても、Titleコンポーネントとか(再レンダリングしても結果同じなやつ)も含め、全てのコンポネントが再レンダリングされます。

console.log仕込んでるので、それがわかります。

8de0ba44bab2230c72d3dddad8c8241a (1).gif

サンプルコード
App.js
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;
components/Title.js
import React from "react";

const Title = () => {
  console.log("Title");
  return <h1>身長と体重の入力</h1>;
};

export default Title;
components/Button.js
import React from "react";

const Button = ({ handleClick, children }) => {
  console.log(`Button - `, children);
  return <button onClick={handleClick}>{children}</button>;
};

export default Button;
components/Count.js
import React from "react";

const Count = ({ text, count }) => {
  console.log(`Count - `, text);
  return (
    <p>
      {text} : {count}
    </p>
  );
};

export default Count;

☝️のサンプルコードの問題点

どのボタンを押しても、全てのコンポネントが再レンダリングされることが、パフォーマンスが悪いとしましょう。

そこで、パフォーマンス改善に役立つReact.memouseCallbackを試してみましょう。

そこで、React.memoの出番

React.memo とはコンポーネントをメモ化(計算結果を再利用するために保持)するReactのAPIです。

キャッシュみたいなもんです。

もう少し細かい言い方をすると、HOCで、React.memoを使うことで、propsの値が変わらないなら、関数コンポネントのレンダリングを抑制することができるAPIです。

なので、以下のようなコンポネントをメモ化するとパフォーマンス上有効です。

  • レンダーコストが高いコンポネント
  • 頻繁に再レンダーされるコンポネントの子コンポネント

やってみましょう。変更点は、赤枠のみ。👇

musing-almeida-g5ogr_-_CodeSandbox.png

身長+1ボタンをクリックしたら...

以下のコンポネントが再レンダリングされます。

  • 身長のCountコンポネント
  • 身長のButtonコンポネント
  • 体重のButtonコンポネント(👈 なんでこいつが再レンダリングされるんだ? してほしくない)

以下のコンポネントは再レンダリングされません。

  • 体重のCountコンポネント
  • Titleのコンポネント

musing-almeida-g5ogr_-_CodeSandbox.png

なぜ「身長+1ボタン」をクリックしたら体重のButtonコンポネントがレンダリングされるのか?

App コンポネントが再レンダリングされるたびに、関数も再生成され、再生成の前後で関数は等価ではありません。

なので、React.memoで子コンポネントをメモ化しても コールバック関数をpropsとして渡す場合は子コンポコンポネントは必ず再レンダリングされます。

この問題を解決するのがuseCallbackです。

次に、usecallbackの出番

変更点は、赤枠。👇

musing-almeida-g5ogr_-_CodeSandbox.png

実行結果

399b7c1abfe524dedf2410cad071f1b0.gif

useCallbackを使うことでコンポネントの再レンダリングを最適化することができました。

このように、子コンポネントにコールバック関数をpropsとして渡す場合は使ってみるといいと思います。

useCallbackの注意点

前述の通り、useCallbackReact.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関数の等価に保たれ、変化しないことを保証します。
従って useEffectuseCallbackの依存リスト(依存配列)にはsetState関数を含めなくてもいいです。

useMemo とは

useCallback同様、パフォーマンス最適化用のHookです。

useCallbackとの違いは以下です。

  • useCallbackは関数自体をメモ化
  • useMemoは関数の結果をメモ化(メモ化された値を返すHook)

useMemoは、値を算出するための不要な再計算をスキップすることでパフォーマンスを向上させます。

サンプルコード

こんなん作るとします。useCallbackの時とほとんど同じです。

身長だけ、奇数か偶数か判定されています。

スクリーンショット 2021-01-28 2.01.17.png

差異があるファイルだけ以下に記載しておきます。

サンプルコード
components/App.js
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が実行されるようにしましょう。

サンプルコード
components/App.js
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;

変更点は、赤枠。👇

musing-almeida-g5ogr_-_CodeSandbox.png

useMemoは、値(今回は bool値)を算出するための不要な再計算(今回なら「体重+1」ボタンクリック時のisEven内の計算処理)をスキップさせています。

「体重+1」ボタンクリック時は、useMemoisEven処理を再実行させずに、結果(bool値)のみを返しています。

仮に、isEven処理が重い場合、「体重+1」ボタンクリック時は、結果(bool値)のみを返すので、パフォーマンス向上につながると思います。

今回は以上です。

参考

16
12
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
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?