React.memo・useCallback()を整理してみた
これは何か?
React初学者が 「React.memo」・「useCallback()」 について学習内容をまとめた記事です。
今回は、カウント機能アプリを例に「React.memo」・「useCallback()」を実装してみます。
React.memo・useCallback()とは何か?
「React.memo」・「useCallback()」を使用することで、不要な再レンダリングを防ぐことができパフォーマンス向上が期待できます。逆に言えば、これらを使用しなくてもアプリーケーション自体は動作します。
React.memo
- 高階コンポーネント
- 前回のpropsと新しいpropsを比較して値をチェックして、値が等価であれば再レンダリングをスキップ、値が等価でなければ再レンダリング
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 [email protected] react-typescript-app --template typescript
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
親コンポーネント
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;
子コンポーネント
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;
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;
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 {
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;
}
}
}
}
.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を子コンポーネントに実装してみます。
+ 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>;
- };
+ 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>
- );
- };
+ 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コンポーネントの関数をメモ化します。
+ 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コンポーネントのみレンダリングされています。
▼イメージ図
以上になります。
最後までお読みいただきあありがとうございました。
もし誤っていることがあれば、ご指摘いただけます幸いです。
参考記事・参考書籍
Author And Source
この問題について(React.memo・useCallback()を整理してみた), 我々は、より多くの情報をここで見つけました https://zenn.dev/masatotezuka/articles/9102b1e260a478著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol