☘️

React.memo・useCallback()を整理してみた

2022/04/24に公開

これは何か?

React初学者が 「React.memo」・「useCallback()」 について学習内容をまとめた記事です。
今回は、カウント機能アプリを例に「React.memo」・「useCallback()」を実装してみます。

React.memo・useCallback()とは何か?

「React.memo」・「useCallback()」を使用することで、不要な再レンダリングを防ぐことができパフォーマンス向上が期待できます。逆に言えば、これらを使用しなくてもアプリーケーション自体は動作します。

React.memo

  • 高階コンポーネント
  • 前回のpropsと新しいpropsを比較して値をチェックして、値が等価であれば再レンダリングをスキップ、値が等価でなければ再レンダリング

https://ja.reactjs.org/docs/higher-order-components.html

React.memoの構文

React.memo(親コンポーネントからpropsを受け取る子コンポーネント)

useCallback()

  • 関数自体をメモ化するフック
  • useCallback()でメモ化されたコールバック関数が、React.memoでメモ化された子コンポーネントへpropsとして渡されることで、不要な再レンダリングのスキップが可能

useCallback()の構文

useCallback(コールバック関数,[コールバック関数が依存している要素の配列])

開発環境

  • OS:macOS Monterey バージョン12.3.1
  • 言語:TypsScript
  • ライブラリー:React

環境構築

今回はNodeをインストールしている前提で進めます。
環境構築するディレクトリに移動し、「Create-React-App」を使用してReactのアプリ環境を構築します。

$ npx create-react-app@5.0.1 react-typescript-app --template typescript

https://create-react-app.dev/docs/getting-started

You are running `create-react-app` 5.0.0, which is behind the latest release (5.0.1).

We no longer support global installation of Create React App.

Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app

The latest instructions for creating a new app can be found here:
https://create-react-app.dev/docs/getting-started/

ファイル構成

react-typescript-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── ts.config.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
        ├──components
    │	├──Button
    │   │  ├──Button.tsx
    │   │  └──Button.module.scss
    │   ├──Counter
    │   │  ├──Counter.tsx
    │	│  └──Counter.module.scss
    │	└──Title
    │      ├──Title.tsx
    │	   └──Title.scss
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── serviceWorker.js
    └── setupTests.js

実装

今回はSassでスタイリングするので、node-sassをインストールします。

$npm install node-sass

親コンポーネント

App.tsx
import React, { useState } from "react";
import classes from "./App.module.scss";
import Title from "./components/Title/Title";
import Counter from "./components/Counter/Counter";
import Button from "./components/Button/Button";

function App() {
  const [countA, setCountA] = useState<number>(0);
  const [countB, setCountB] = useState<number>(0);

  const handleCountA = () => {
     setCountA((preCount) => preCount + 1);
   };
  const handleCountB = () => {
     setCountB((preCount) => preCount + 1);
   };

  return (
    <div className={classes.app}>
      <Title titleText="ボタンを押した回数"></Title>
      <div className={classes.itemsWrapper}>
        <div className={classes.items}>
          <div className={classes.countsWrapper}>
            <Counter countText="Aボタン" count={countA}></Counter>
            <Counter countText="Bボタン" count={countB}></Counter>
          </div>
          <div className={classes.buttonsWrapper}>
            <Button buttonText="A" onClick={handleCountA} />
            <Button buttonText="B" onClick={handleCountB} />
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;

子コンポーネント

componets/Title/Title.tsx
import React from "react";

type Props = {
  titleText: string;
};

const Title: React.VFC<Props> = ({ titleText }) => {
  console.log(`Title:${titleText}`);
  return <h2>{titleText}</h2>;
};

export default Title;
components/Counter/Counter.tsx
import React from "react";

type Props = {
  count: number;
  countText: string;
};

const Counter: React.VFC<Props> = ({ countText, count }) => {
  console.log(`${countText}ボタンを押した回数:${count}`);
  return (
    <h2>
      {countText}を押した回数:{count}</h2>
  );
};

export default Counter;
components/Button/Button.tsx
import React from "react";
import classes from "./Button.module.scss";

type Props = {
  buttonText: string;
  onClick: () => void;
};

const Button: React.VFC<Props> = ({ buttonText, onClick }) => {
  console.log(`${buttonText}ボタンをクリック`);
  return (
    <div className={classes.container}>
      <button className={classes.button} onClick={onClick}>
        {buttonText}
      </button>
    </div>
  );
};

export default Button;

スタイリング

App.module.scss
.app {
  margin-top: 40px;
  text-align: center;
  font-family: "Courier New", Courier, monospace;
  .itemsWrapper {
    .items {
      margin-left: auto;
      margin-right: auto;
      width: 70%;
    }
    .countsWrapper {
      display: flex;
      justify-content: space-around;
    }
    .buttonsWrapper {
      display: flex;
      justify-content: space-around;
      .buttons {
        text-align: center;
      }
    }
  }
}
components/Button/Button.module.scss
.container {
  margin-left: 10px;
  .button {
    background-color: gray;
    color: white;
    font-weight: bold;
    width: 100px;
    padding: 10px 20px;
    font-size: 24px;
    cursor: pointer;
    border: none;
    border-radius: 10px;
  }
}

完成画面

では、Aボタンを押した時のログを確認しながら、React.memoとuseCallback()を実装した時としていない時の違いをみていきます。

React.memoを実装しない場合

Aボタンをクリックしてみます。

すべての子コンポーネントが再レンダリングされていることがわかります。

▼イメージ図(あくまでもイメージ図なので、実際の構築環境とは違うことをご了承ください。)

React.memoを実装した場合

React.memoを子コンポーネントに実装してみます。

components/Title/Title.tsx
+ const Title: React.VFC<Props> = React.memo(({ titleText }) => {
+  console.log(`Title:${titleText}`);
+  return <h2>{titleText}</h2>;
+ });
- const Title: React.VFC<Props> = { titleText }) => {
-  console.log(`Title:${titleText}`);
-  return <h2>{titleText}</h2>;
- };
components/Counter/Counter.tsx
+ const Counter: React.VFC<Props> = React.memo(({ countText, count }) => {
+ console.log(`${countText}ボタンを押した回数:${count}`);
+ return (
+ <h2>
+     {countText}を押した回数:{count}+  </h2>
+ );
+ });
- const Counter: React.VFC<Props> = ({ countText, count }) => {
- console.log(`${countText}ボタンを押した回数:${count}`);
- return (
-    <h2>
-      {countText}を押した回数:{count}-    </h2>
-  );
- };
components/Button/Button.tsx
+ const Button: React.VFC<Props> = React.memo(({ buttonText, onClick }) => {
+ console.log(`${buttonText}ボタンをクリック`);
+ return (
+   <div className={classes.container}>
+     <button className={classes.button} onClick={onClick}>
+       {buttonText}
+     </button>
+   </div>
+  );
+ });
- const Button: React.VFC<Props> = ({ buttonText, onClick }) => {
- console.log(`${buttonText}ボタンをクリック`);
- return (
-    <div className={classes.container}>
-      <button className={classes.button} onClick={onClick}>
-        {buttonText}
-      </button>
-    </div>
-  );
- };

Aボタンをクリックしてみます。

TitleコンポーネントとBのCouterコンポーネントはレンダリングされていません。これは、React.memoで子コンポーネントがメモ化され、前回と今回のPorpsの差分が変更がなかった場合に子コンポーネントはレンダリングされないからです。
しかし、BのButtonコンポーネントはレンダリングされています。理由は、Appコンポーネントでレンダリングされた時に「handleCountB」関数が再作成されるため、Buttonコンポーネントに渡された「onClick」は、前回の「onClick」とは等価でないとされてしまいレンダリングされるためです。「handleCountB」関数が再作成されないようにするためには、useCallbackを使用して、「handleCountB関数」をメモ化する必要があります。

▼イメージ図

React.memo・useCallback()を実装した場合

useCallbackを使用して、Appコンポーネントの関数をメモ化します。

App.tsx
+ import React, { useCallback, useState } from "react";
- import React, { useState } from "react";

+ const handleCountA = useCallback(() => {
+  setCountA((preCount) => preCount + 1);
+ }, []);
- const handleCountA = () => {
-    setCountA((preCount) => preCount + 1);
-  };

+ const handleCountB = useCallback(() => {
+   setCountB((preCount) => preCount + 1);
+ }, []);
- const handleCountB = () => {
-   setCountB((preCount) => preCount + 1);
- };

Aボタンをクリックしてみます。

AのButtonコンポーネントのみレンダリングされています。

▼イメージ図

以上になります。
最後までお読みいただきあありがとうございました。
もし誤っていることがあれば、ご指摘いただけます幸いです。

参考記事・参考書籍

https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2

https://www.amazon.co.jp/基礎から学ぶ-React-Hooks-asakohattori/dp/486354359X

GitHubで編集を提案

Discussion