Reactコンポーネントの再レンダリング発生条件と防止方法

JavaScript

コンポーネントが再レンダリングされる条件について

コンポーネントは以下の条件のいずれかに当てはまると再レンダリングされます。

コンポーネントの再レンダリング条件
  • コンポーネントのStateが更新された時
  • 親コンポーネントがレンダリングされた時

以下では再レンダリングが発生する具体例について紹介します。

コンポーネントのStateが更新された時

以下のExample.tsxはStateでcountという数値を管理しています。countの更新に応じてExample.tsxはレンダリングされます。

Example.tsx

import { useEffect, useState } from "react";
import "./Example.css";

const Example: React.FC = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('useEffect is called')
    // レンダリングの変化をわかりやすくするように非同期にしている
    const increment = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setCount((prevCount) => prevCount + 1);
    };
    increment();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log("Example.tsx: render");

  return <div className="Example">{count}</div>;
};

export default Example;

実行ログ

Example.tsx: count is 0
Example.tsx: render

useEffect is called

Example.tsx: count is 1
Example.tsx: render

上記で紹介したExample.tsxの時間経過による変化は以下の通りです。

  1. 初回レンダリングされる
  2. マウントされる
  3. 画面が表示される(このときcountは0)
  4. マウント後、useEffectが実行される
  5. useEffectによってStateが更新される
  6. Stateが更新されたので再度レンダリングされる
  7. 画面が更新される(このときcountは1)

参考: 複数のStateを持つ場合、各Stateが更新されるたびにレンダリングが実行される

コンポーネントが複数のStateを管理する場合は各Stateが変化するたびにレンダリングが実行されます。

以下のExample.tsxはStateでcountという数値とisDisplayという真偽値を管理しています。countあるいはisDisplayが更新されるたびにExample.tsxはレンダリングされます。

Example.tsx

import { useEffect, useState } from "react";
import "./Example.css";

const Example: React.FC = () => {
  const [count, setCount] = useState(0);
  const [isDisplay, setIsDisplay] = useState(false);

  useEffect(() => {
    const increment = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setCount((prevCount) => prevCount + 1);
    };
    increment();
  }, []);

  useEffect(() => {
    const displayable = async () => {
      await new Promise((s) => setTimeout(s, 4000));
      setIsDisplay(true);
    };
    displayable();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log(`Example.tsx: isDisplay is ${isDisplay}`);
  console.log("Example.tsx: render");

  return (
    <div className="Example">
      {count}
      {isDisplay && <div>Now you can see the description!</div>}
    </div>
  );
};

export default Example;

実行ログ

Example.tsx: count is 0
Example.tsx: isDisplay is false
Example.tsx: render

Example.tsx: count is 1
Example.tsx: isDisplay is false
Example.tsx: render

Example.tsx: count is 1
Example.tsx: isDisplay is true
Example.tsx: render

親コンポーネントがレンダリングされた時

Propsの受け取り有無に関わらず、親コンポーネントのレンダリングに応じて子コンポーネントもレンダリングされます。

具体例は以下の通りです。

Example.tsx

import { useEffect, useState } from "react";
import "./Example.css";
import "./ExampleChild";
import ExampleChild from "./ExampleChild";

const Example: React.FC = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const increment = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setCount((prevCount) => prevCount + 1);
    };
    increment();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log("Example.tsx: render");

  return (
    <div className="Example">
      {count}
      <ExampleChild />
    </div>
  );
};

export default Example;

ExampleChild.tsx

import "./ExampleChild.css";

const ExampleChild: React.FC = () => {
  console.log("ExampleChild.tsx: render");

  return <div className="ExampleChild">This is ExampleChild.tsx</div>;
};

export default ExampleChild;

実行ログ

Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render

Example.tsx: count is 1
Example.tsx: render
ExampleChild.tsx: render

親コンポーネントのレンダリングによるコンポーネントの再レンダリングを防ぐ方法

親コンポーネントの変更が子コンポーネントに影響を与えない場合、親コンポーネントのレンダリングに応じて子コンポーネントを再レンダリングさせる必要がありません。

親コンポーネントのレンダリングによる再レンダリングを制御する方法には以下の2つがあります。

  • コンポーネントをメモ化する
  • コンポジション(props.children)を利用する

以下ではそれぞれについて紹介します。

コンポーネントをメモ化する

コンポーネントのメモ化をするにはReact.memoもしくはuseMemoを利用します。

React.memoとuseMemoの詳細解説は具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニングで紹介しています。

React.memoを利用する方法

React.memoはクラスコンポーネントにおけるshouldComponentUpdate()React.PureComponentに相当する、コンポーネントをメモ化する機能です。

React.memoは高階コンポーネント(Higher-Order Component, HOC)の一種ですので、React.memo(コンポーネント)という形でコンポーネントを囲むことで利用できます。

React.memoを利用したコンポーネントのメモ化は以下の通りです。

ExampleChild.tsx

+ import { memo } from "react";
import "./ExampleChild.css";

const ExampleChild: React.FC = () => {
  console.log("ExampleChild.tsx: render");

  return <div className="ExampleChild">This is ExampleChild.tsx</div>;
};

- export default ExampleChild;
+ export default memo(ExampleChild);

実行ログ

Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render

Example.tsx: count is 1
Example.tsx: render

useMemoを利用する方法

useMemoは関数の結果をメモ化するフックです。

useMemoはuseMemo(() => 結果を算出するロジック, 依存配列)という形で利用します。
依存配列の要素のうちいずれかが変化した場合のみ再計算されます。
依存配列が設定されていない場合はレンダリングのたびに計算が実行されます。

useMemoはReact.memoのようにコンポーネントのメモ化もできます。

useMemoを利用したコンポーネントのメモ化は以下の通りです。

Example.tsx

+ import { useEffect, useMemo, useState } from "react";
- import { useEffect, useState } from "react";
import "./Example.css";
import "./ExampleChild";
import ExampleChild from "./ExampleChild";

const Example: React.FC = () => {
  const [count, setCount] = useState(0);
+ const ExampleChildMemo = useMemo(() => <ExampleChild />, []);
  useEffect(() => {
    const increment = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setCount((prevCount) => prevCount + 1);
    };
    increment();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log("Example.tsx: render");

  return (
    <div className="Example">
      {count}
+     {ExampleChildMemo}
-     <ExampleChild />
    </div>
  );
};

export default Example;

実行ログ

Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render

Example.tsx: count is 1
Example.tsx: render

参考: コールバック関数をPropsで受け取る場合はuseCallbackを併用する

関数コンポーネント内で定義したアロー関数はレンダリングのたびに再作成されます。

ですので、コールバック関数をPropsで受け取る場合はコンポーネント自身をメモ化しただけではメモ化がうまく機能しません。

なぜなら、親コンポーネントがレンダリングされるたびにコールバック関数が新たに作成されるため、メモ化した子コンポーネントのProps(依存配列)が変化したとみなされるからです。
その結果、親コンポーネントがレンダリングされるたびに子コンポーネントもレンダリングされます。

コールバック関数をPropsで受けとるコンポーネントをメモ化する場合は、コンポーネントだけでなくコールバック関数もメモ化する必要があります。
コールバック関数のメモ化はuseCallbackを利用します。

具体例は以下の通りです。

+ import { useCallback, useEffect, useMemo, useState } from "react";
- import { useEffect, useState } from "react";
import "./Example.css";
import ExampleChild from "./ExampleChild";

const Example: React.FC = () => {
  const [count, setCount] = useState(0);
  const [isDisplay, setIsDisplay] = useState(false);


// コールバック関数のメモ化
+ const handleClick = useCallback(() => {
+   setCount((prevCount) => prevCount + 1);
+ }, []);

- const handleClick = () => {
-   setCount((prevCount) => prevCount + 1);
-  };

// コールバック関数をメモ化しているため、
// 親コンポーネントがレンダリングされてもコールバック関数は再作成されない
// その結果、コンポーネントのメモ化が正常に機能する
+ const ExampleChildMemo = useMemo(
+   () => <ExampleChild onClick={handleClick} />,
+   [handleClick]
+ );

  useEffect(() => {
    const displayable = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setIsDisplay(true);
    };
    displayable();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log(`Example.tsx: isDisplay is ${isDisplay}`);
  console.log("Example.tsx: render");

  return (
    <div className="Example">
      {count}
      {isDisplay && <div>Now you can see the description!</div>}
+     {ExampleChildMemo}
-     <ExampleChild onClick={handleClick} />
    </div>
  );
};

export default Example;

ExampleChild.tsx

import "./ExampleChild.css";

type ChildProps = {
  onClick: () => void;
};

const ExampleChild: React.FC<ChildProps> = ({ onClick }) => {
  console.log("ExampleChild.tsx: render");

  return (
    <div className="ExampleChild">
      <button onClick={onClick}>count+</button>
    </div>
  );
};

export default ExampleChild;

実行ログ

Example.tsx: count is 0
Example.tsx: isDisplay is false
Example.tsx: render
ExampleChild.tsx: render

Example.tsx: count is 0
Example.tsx: isDisplay is true
Example.tsx: render
#
# useCallbackを利用しないと isDisableがtrueに変化したこのタイミングでExampleChildが再レンダリングされる
#

useCallbackの詳細解説は具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニングで紹介しています。

コンポジション(props.children)を利用する

親コンポーネントにコンポジションモデルを採用し、親コンポーネントから子コンポーネントをprops.childrenで呼び出します。
コンポジションを利用することで親コンポーネントのレンダリングによる再レンダリングを防げます。

具体例は以下の通りです。

index.tsx

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import Example from "./Example";
+ import ExampleChild from "./ExampleChild";

ReactDOM.render(
  <React.StrictMode>
+   <Example>
+     <ExampleChild />
+   </Example>
-   <Example />
  </React.StrictMode>,
  document.getElementById("root")
);

Example.tsx

import { useEffect, useState } from "react";
import "./Example.css";
- import "./ExampleChild";
- import ExampleChild from "./ExampleChild";

+ const Example: React.FC = ({ children }) => {
- const Example: React.FC = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const increment = async () => {
      await new Promise((s) => setTimeout(s, 2000));
      setCount((prevCount) => prevCount + 1);
    };
    increment();
  }, []);

  console.log(`Example.tsx: count is ${count}`);
  console.log("Example.tsx: render");

  return (
    <div className="Example">
      {count}
+     {children}
-     <ExampleChild />
    </div>
  );
};

export default Example;

実行ログ

Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render

Example.tsx: count is 1
Example.tsx: render

今回のまとめ

コンポーネントの再レンダリング条件と防止方法
  • コンポーネントが再レンダリングされるトリガは『Stateの更新』もしくは『親コンポーネントの更新』
  • 親コンポーネントの更新による再レンダリングを防ぐには『コンポーネントのメモ化』もしくは『コンポジションモデルの採用』をする
  • コンポーネントをメモ化するにはReact.memoもしくはuseMemoを利用する
  • コールバック関数をPropsで受け取るコンポーネントをメモ化する際はuseCallbackで関数もメモ化する必要がある

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料