Michi's Tech Blog

一人前のWebエンジニアを目指して

Reactキャッチアップへの道② 『レンダリングの最適化』

こんにちは!
スマレジ テックファームのMichiです!

前週に引き続き、Reactのキャッチアップをしていきます。今回は、レンダリングの最適化』について学びます。

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

前回のおさらいです。Reactにおいて、コンポーネントの再レンダリングが発生する条件は、(大まかに言って)以下の三つでした。

  1. コンポーネント内の状態管理されている値(state)が変更されたとき
  2. コンポーネントからプロパティ(props)を受け取っている場合、そのプロパティ値が変更されたとき
  3. コンポーネントが再レンダリングされたとき

今回はその中でも、3. 親コンポーネントが再レンダリングされたときの挙動について見ていきます。

サンプルコード

次のようなサンプルコードを用意します。

ひとつめのParent.jsxは、外部からの入力値を受け付けるフォームと、子コンポーネントChild.jsxの表示切り替えるボタンを持ちます。

ふたつめのChild.jsxは、Parent.jsxから受け取ったpropsの値を使用して、コンポーネントの中身を表示するかどうかを決定しています。

Parent.jsx

import { useState } from "react";
import { Child } from "./Child";

export const Parent = () => {
  const [text, setText] = useState("");
  const [isShow, setIsShow] = useState(false);

  const onChangeText = (e) => setText(e.target.value);
  const onClickShow = () => setIsShow(!isShow);

  return (
    <div>
      <input value={text} onChange={onChangeText} />
      <button onClick={onClickShow}>表示</button>
      <Child isShow={isShow} />
    </div>
  );
}

Child.jsx

export const Child = (props) => {
  const { isShow } = props;
  console.log("Childがレンダリングされました");

  return (
    <>
      {isShow ? (
        <div>
          <p>子コンポーネント</p>
        </div>
      ) : null}
    </>
  );
};


画面からフォームに値を入力するたびに、Child.jsxが再レンダリングされ、コンソールに「Childがレンダリングされました」のメッセージが表示されます。 これは、親のParent.jsx内で、textの値に変更が生じたために再レンダリングが発生し、その影響で子のChild.jsxも同時に再レンダリングされてしまうためです。

今の状況は、Child.jsx内の値は何も更新されていないにもかかわらず、再レンダリングが走っています。これは不要なレンダリングなので、起こさないようにしたいです。

memo - コンポーネントのメモ化 -

親から渡されたpropsの値に変更が生じたとき以外、コンポーネントの再レンダリングを起こさないようにする機能がmemoです。

使い方は簡単で、Reactからmemoをインポートし、適用したいコンポーネントをラップするだけです。

Child.jsx

import {memo} from "react"

// 適用したいコンポーネントをmemoでラップする
export const Child = memo((props) => {
  const { isShow } = props;
  console.log("Childがレンダリングされました");

  return (
    <>
      {isShow ? (
        <div>
          <p>子コンポーネント</p>
        </div>
      ) : null}
    </>
  );
});


先ほどは、フォームに値を入力するたびにコンソールにメッセージが表示されていましたが、 今回は最初に画面を読み込んだタイミング、もしくは、『表示』ボタンをクリックしてpropsの値が変更された時にしか、メッセージが表示されていません。親コンポーネントが再レンダリングされても、子コンポーネントは再レンダリングされていないことが分かります。

このように、ある状態を記憶(キャッシュ)して、不要な再計算をスキップさせることを、Reactでは「メモ化する」と言ったりします。

useCallBack - 関数のメモ化 -

先ほどのmemoでは、コンポーネント全体をメモ化しました。これに対して、特定の関数のみをメモ化する機能がuseCallbackです。

memoの解説で使用したサンプルコードに機能を追加します。Child.jsx内に『閉じる』ボタンを設置し、クリックするとChild.jsxの内容が非表示になるようにします。

Parent.jsx

import { useState } from "react";
import { Child } from "./Child";

export const Parent = () => {
  const [text, setText] = useState("");
  const [isShow, setIsShow] = useState(false);

  const onChangeText = (e) => setText(e.target.value);
  const onClickShow = () => setIsShow(!isShow);
  // 『閉じる』機能用の関数
  const onClickClose = () => setIsShow(false)

  return (
    <div>
      <input value={text} onChange={onChangeText} />
      <button onClick={onClickShow}>表示</button>
      {/* onClickCloseをpropsで渡す */}
      <Child isShow={isShow} onClickClose={onClickClose}/>
    </div>
  );
}

Child.jsx

import {memo} from "react"

export const Child = memo((props) => {
  const { isShow, onClickClose } = props;
  console.log("Childがレンダリングされました");

  return (
    <>
      {isShow ? (
        <div>
          <p>子コンポーネント</p>
          {/* 閉じるボタンを追加する */}
          <button onClick={onClickClose}>閉じる</button>
        </div>
      ) : null}
    </>
  );
});

Child.jsxの表示を決定するフラグisShowは親コンポーネント側にあるので、フラグの値をfalseにするための関数onClickCloseParent.jsx内で定義し、propsで関数ごと子コンポーネントに渡します。

するとどうでしょう。再び、フォームに値を入力するたびに再レンダリングが走るようになりました。(Child.jsxはメモ化しているのにもかかわらずです!)

実は、関数宣言においては、中身や返り値の値に変化がなくとも、再レンダリングが走るたびに、毎回新しい関数を生成する仕組みになっています。

新しい関数onClickCloseが生成されたので、propsの値も更新されているとReactが判断し、子コンポーネントにも再レンダリングの処理が走るわけです。このような場合は、useCallbackを使用し、関数のメモ化を行う必要があります。

使い方はmemoのときと同じく、ReactからuseCallbackをインポートし、適用したい関数をラップします。memoと違うのは、第二引数に依存配列を取るところです。今回は、関数onClickCloseは初期値から変更されることはないので、依存配列の中身は空([])で設定します。

Parent.jsx

import { useState, useCallback } from "react";
import { Child } from "./Child";

export const Parent = () => {
  const [text, setText] = useState("");
  const [isShow, setIsShow] = useState(false);

  const onChangeText = (e) => setText(e.target.value);
  const onClickShow = () => setIsShow(!isShow);
  // 閉じる機能の関数をメモ化する
  const onClickClose = useCallback(() => setIsShow(false), []);

  return (
    <div>
      <input value={text} onChange={onChangeText} />
      <button onClick={onClickShow}>表示</button>
      {/* onClickCloseをpropsで渡す */}
      <Child isShow={isShow} onClickClose={onClickClose} />
    </div>
  );
};


これで、フォームに値を入力したときでも、再び再レンダリングが走らないようになりました。

useMemo - 変数のメモ化 -

最後に、変数のメモ化についても軽く触れておきます。

変数をメモ化するには、useMemoを使用します。
使い方はuseCallbackとまったく同じです。例えば、次のように定義することで、初期レンダリング時のみ1 + 2の計算を行い、再レンダリング時にはキャッシュ値を使用する(再計算を行わない)ようにすることができます。

const sample = useMemo(() => 1 + 2, []);

正直、この程度の計算では、パフォーマンスに違いはほぼ出ません。もっと複雑で大きい計算が必要な時に、useMemoが効力を発揮します。

まとめ

Reactキャッチアップの2週目は、Reactのレンダリング最適化についてのアウトプットでした。
Vue.jsに慣れた自分としては、わざわざ手動で最適化しなければならないのが面倒くさいなと思ったりするのですが、そこがマスターできればReact初心者卒業なんでしょうね(^_^;)