React でコンポーネントの再レンダーを防ぐ仕組みを理解するミニマムな例


こんにちはフロントエンドエンジニアの茶木です。

React コンポーネントのレンダーの基本を理解し、再レンダーの抑制の方法をまとめました。

先日、社内のテックシェアで React の hooks の基礎の勉強会をしたのですが、その中で useCallback を使った、再レンダーを防ぐ仕組みを説明したかったのですが、わかりやすい例を準備できなかったのでこちらのブログでリベンジします。

React コンポーネントの再レンダーの基本

コンポーネントの再レンダーは、以下の場合に発生します。

  • state に変更があった場合
  • props に変更があった場合
  • 親コンポーネントでレンダリングがあった場合

再レンダーが起きる例

実際の実装に近い形かつミニマムな例を考えてみました。

text input と reset を持ったフォームです。
これはちょっとありそうな例だと思います。

import React, { useState } from "react";

interface ResetProp {
  reset: () => void;
}
const Reset = ({ reset }: ResetProp) => {
  console.log("Reset: render");
  return <button onClick={reset}>リセット</button>;
};

const Form = () => {
  console.log("Form: render");
  const [text, setText] = useState("");
  const onReset = () => {
    setPassword("");
  };
  return (
    <>
      <input
        value={text}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          setText(e.target.value);
        }}
      />
      <Reset reset={onReset} />
    </>
  );
};
export default Form;

挙動

テキスト入力もリセットボタンでテキストの削除も問題なく動作します。
しかし、テキスト入力ごとにリセットボタンコンポーネントのレンダーが起きます。

リセットボタンコンポーネントは、このレンダー前とレンダー後で見た目も機能も変わらないので再レンダーは必要ありません。これを抑制します。

再レンダー発生までの流れ

再レンダーまでの流れをまとめます。

  1. テキスト入力をする
  2. input の onChange がコールされる
  3. onChange の中で setText がコールされる
  4. setText によって text つまり state が変更される
  5. state の変更によって Form コンポーネントが再レンダーされる
  6. Reset コンポーネントの再レンダー条件がそろう
    条件a. 親コンポーネントでレンダリングがあった
    条件b. props に変更があった

ここで、Reset コンポーネントの再レンダーが発生する条件が2つ発生します。どちらかでも満たしていれば再レンダーが発生するので、両方とも解消する必要があります。

React.memo を 使う

React.memo は親コンポーネントの変化を吸収します。
第一引数は、コンポーネントの props に相当します。これで条件a を解消しました。

import React, { memo } from "react";
const Reset = memo(({ reset }: ResetProp) => {
  console.log("reset: rendered");
  return <button onClick={reset}>リセット</button>;
});
Reset.displayName = "Reset";

Reset.displayName = "Reset";
は、memo を使うと displayName がセットされずに環境によっては警告がでるので、セットしています。

条件bを理解する

上記の React.memo を設定した状態で実行しても変わらず再レンダーは走ります。
これは条件b の Reset コンポーネントの props の変更が原因です。

再レンダー発生までの流れをもう一度確認すると、

  1. テキスト入力をする
  2. input の onChange がコールされる
  3. onChange の中で setText がコールされる
  4. setText によって text つまり state が変更される
  5. state の変更によって Form コンポーネントが再レンダーされる
  6. Reset コンポーネントの再レンダー条件がそろう
    条件a. 親コンポーネントでレンダリングがあった
    条件b. props に変更があった

5で Form コンポーネントが再レンダーされたときに、

  const onReset = () => {
    setPassword("");
  };

onReset再度宣言されます。
この onReset が Reset コンポーネントの props.reset にセットされます。

props.reset の値が変わり 条件b が満たされました。

useCallback を使う

onReset を固定化することで、props.reset にセットする値が変わらないようにします。

  const onReset = useCallback(() => {
    setText("");
  }, []);

これで、条件b を止めました。

全部のコード

以下が、Reset フォームが テキストの入力によって再レンダーされるのを抑制したコードになります。

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

interface ResetProp {
  reset: () => void;
}
const Reset = memo(({ reset }: ResetProp) => {
  console.log("reset: rendered");
  return <button onClick={reset}>リセット</button>;
});
Reset.displayName = "Reset";

const Form = () => {
  console.log("Form: render");
  const [text, setText] = useState("");
  const onReset = useCallback(() => {
    setText("");
  }, []);
  return (
    <>
      <input
        value={text}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          setText(e.target.value);
        }}
      />
      <Reset reset={onReset} />
    </>
  );
};
export default Form;

まとめ

親要素 Form コンポーネントの再レンダー時の子要素 Reset コンポーネントの再レンダーの抑制の方法を書きました。

ただ、通常は再レンダーは React に任せておいても問題ないのでパフォーマンス上の問題がでるコンポーネントに対してのみ再レンダーの抑制は対応するのが良いと思います。


投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。