💬

React 再レンダリング (useCallback useMemo)

2023/11/30に公開

はじめに

最近 React の再レンダリングについて理解が深まったので少しまとめようと思いました。
フロントエンドとバックエンドを兼業しているとどちらの知識も薄くなりがちで意外と基本的な部分を抑えるのに時間がかかる、、、

再レンダリングの仕組み

ここの記事でレンダリングの仕組みを詳しく説明しているので参考にしてください。ここでは再レンダリングの仕組みを解説します。
https://zenn.dev/tmk616/articles/2185254ba8703f

主には再レンダリングのタイミングは以下のとおりです。

  • stateに変化が起きた時
  • 親コンポーネントが再レンダリングされた時

stateに変化が起きた時

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((count) => count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

こちらが実際の動きになります。

button をクリックすると count が増えて、state が更新される処理です。React dev toolで確認すると再レンダリングされていることが確認できます。

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

import { useState } from "react";

const Child = () => {
  return <div>child</div>;
};

const App = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((count) => count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+</button>
      <Child />
    </div>
  );
};

export default App;

こちらが実際の動きになります。

上と同様にbutton をクリックすると count が増えて、state が更新される処理です。React dev toolで再レンダリングされていることが確認でき、子コンポーネントのChildも再レンダリングされています。

再レンダリングを防ぐ方法

上記の子コンポーネントに関しては特に変更がないのに再レンダリングされています。この再レンダリングは不要であり、このような無駄な再レンダリングがパフォーマンスを下げる原因になったりします。ここからいくつかの再レンダリングを防ぐ例を上げていきます。

React.memo を使う方法

親コンポーネントが再レンダリングされた項目でも記載しましたが、無駄な再レンダリングはパフォーマン図を下げる原因になります。そこで React.memoというものを紹介します。これを使うと親コンポーネントが再レンダリングされたときに子コンポーネントの再レンダリングを防ぐことができます。実際にコードを見てみましょう。

import React, { useState } from "react";

const Child = React.memo(() => {
  return <div>child</div>;
});

const App = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((count) => count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+</button>
      <Child />
    </div>
  );
};

export default App;

こちらが実際の動きになります。

このように子コンポーネントを React.memo を使って定義し直すとChildは再レンダリングされなくなりました。

useCallback を使う方法

関数を子コンポーネントにわたすような場合にも注意する必要があります。

const Child: React.FC<ChildProps> = ({ onClick }) => {
  console.log("Child is rendering");
  return <button onClick={onClick}>Increase Count</button>;
};

const App = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    console.log("Count increased");
    setCount((count) => count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
};

export default App;

上記のようにhandleClickをそのまま渡してしまうと意図しない無駄なレンダリングがされてしまいます。この無駄なレンダリングを防ぐために useCallback を使い以下のように修正しました。

import React, { useState, useCallback } from "react";

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

const Child: React.FC<ChildProps> = ({ onClick }) => {
  console.log("Child is rendering");
  return <button onClick={onClick}>Increase Count</button>;
};

const App = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Count increased");
    setCount((count) => count + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
};

export default App;

こちらが実際の挙動です。

これで再レンダリングの挙動を確認すると、なぜか子コンポーネントも再レンダリングされている、、、

これは、handleClickをメモ化して再レンダリングを防ぐためのものですが、Childコンポーネント自体は再レンダリングを防げていないのでコンポーネント自体もメモ化する必要があります。

import React, { useState, useCallback } from "react";

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

const Child: React.FC<ChildProps> = React.memo(({ onClick }) => {
  console.log("Child is rendering");
  return <button onClick={onClick}>Increase Count</button>;
});

const App = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Count increased");
    setCount((count) => count + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
};

export default App;

こちらが実際の挙動になります。

上記のこうにコードを修正するとメモ化が防ぐことができました。

アロー関数でも再レンダリングので注意!!(補足)

実際に業務で自分がやっていて指摘されたことを上げます。以下のように onClick={() => handleClick()} とやってしまうとせっかくメモ化した関数を再生成され、無駄なレンダリングをしてしまうので注意してください。

import React, { useState, useCallback } from "react";

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

const Child: React.FC<ChildProps> = React.memo(({ onClick }) => {
  console.log("Child is rendering");
  return <button onClick={onClick}>Increase Count</button>;
});

const App = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Count increased");
    setCount((count) => count + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={() => handleClick()} />
    </div>
  );
};

export default App;

Discussion