1
0

More than 3 years have passed since last update.

React FCの中で別のFCを定義するな。絶対にだ!

Last updated at Posted at 2019-11-16

TL,DR

React FC(Functional Component)の中で別のFCを定義しないように気をつけましょう。

やってはいけない実装
const Hoge: React.FC<{ username: string }> = ({ username }) => {
  // FugaはHoge FCの中で定義されている。
  const Fuga: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
    <div>
      <div>こんにちは、{username}さん!</div>
      <div>{children}</div>
    </div>
  );
  return (
    <Fuga>
      <SomeLargeContent />
    </Fuga>
  )
};

FCの中で別のFCを定義することによる問題とその調査

何が起こったのか。

上の例の<SomeLargeContent />の中にて、DOMがユーザー入力によるstateの更新の度に削除と再構築をしてしまい、パフォーマンスやユーザビリティの劣化等、予期せぬ挙動を引き起こしました。

クリティカルな現象としては以下が挙げられると思います。

  • transitionを使用したアニメーションを定義したが、stateの変更によるスタイルの切り替えをしようとする度にDOMが再構築されるのでアニメーションが発生しない。
  • inputにユーザーが入力を行うとinputのDOMが再構築されてフォーカスがinputから外れる。
  • ComponentDidMountやuseEffectがstateの変更の度に発火されてしまう。

そもそも通常の場合は何故DOMの再構築が行われないのか

Reactはrenderによって生成された仮想DOMを前回生成された仮想DOMと比較し、その差分のみDOMを更新・削除・追加しています。なので、仮想DOMがReactに前回の仮想DOMと同一と判断されたDOMについては再構築が行われません。
参考記事

では、この場合はどうだったのか。

Reactの仮想DOMオブジェクトをのぞいてみましょう。

  const Fuga: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
    <div>
      <div>こんにちは、{username}さん!</div>
      <div>{children}</div>
    </div>
  );
  const vdom = (
    <Fuga>
      <SomeLargeContent />
      {/* stateの更新用にチェックボックスを配置 */}
      <input
        type="checkbox"
        checked={state}
        onChange={() => setState(s => !s)}
      />
    </Fuga>
  );
  console.log(vdom)//仮想DOMを出力;

以下
image.png

key,props,typeというプロパティを持っていることがわかります。
typeに注目してみましょう。Fuga関数が格納されています。
仮想DOMは「どのようなDOMが構築されるか」以外にも「どのコンポーネントからDOMが生成されたのか」をtypeとして保持するようになっており、Reactはその差分も検出しています。
つまり、最終的に構築される実DOMに差分が無かったとしても、DOMを構築するコンポーネントが異なっていれば、それは異なるDOMツリーだと解釈されます(当然といえば当然)
FC(1)の中でFC(2)を定義した場合、FC(2)はrenderが走る度に定義されるので、前回のrenderと異なる仮想DOMを生成してしまいます。
これにより、実DOMの差分以上にDOMの再構築が発生してしまうのです。

まとめ

コンポーネントを定義するときは必ず、トップレベルに定義することをお勧めします。
ルートに近いコンポーネントがこれをやらかすとアプリ全ての末端のコンポーネントまで被害を被りますので(実体験)気をつけましょう。

おまけ:仮想DOMをハックしてDOMの再構築を食い止める

Reactは仮想DOMオブジェクトのtypeから仮想DOMが異なるコンポーネントが作られているか判断します。
これを利用しtypeをハックすることでReactに差分を誤認識させ、再構築を食い止めてみましょう。

  const vdom = (
    <Fuga>
      <SomeLargeContent />
      {/* stateの更新用にチェックボックスを配置 */}
      <input
        type="checkbox"
        checked={state}
        onChange={() => setState(s => !s)}
      />
    </Fuga>
  );
  console.log(vdom);
  //初回に定義されたFugaコンポーネントをrefに格納しておく
  const ref = React.useRef<React.FC | null>(null);
  const firstFuga = ref.current;
  if (!firstFuga) {
    ref.current = Fuga;
  }
  //2回目以降のrenderではtypeを初回に格納したFuga関数に差し替える
  return firstFuga
    ? {
        ...vdom,
        type: firstFuga
      }
    : vdom;

絶対に真似しないように。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0