31
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactのレンダリングの仕組みを整理する

31
Posted at

前回と前々回で、Reactにおける stateref について整理してきました。

  • state は、画面に反映したい値を持つもの
  • ref は、再レンダリングを発生させずに値やDOMを保持するもの

この2つを整理すると、 「そもそもReactのレンダリングとは何をしているのか」 という点が気になります。
そのため今回は、Reactのレンダリングについて、整理しようと思います。

1. レンダリングとは何か

Reactにおけるレンダリングとは、Reactがコンポーネントを実行し、次にどのようなUIを表示するかを計算することです。

ここで最初に押さえておきたいのは、レンダリングと実際の画面更新は同じ意味ではないという点です。

Reactの更新は大きく2つのフェーズに分かれます。

  1. レンダリング — コンポーネントを実行して、次に表示したいUIを計算する
  2. コミット — 計算結果をもとに、必要な変更を実際のDOMへ反映する

この2つを分けて考えることが、Reactの挙動を理解する第一歩です。

2. Reactが画面を更新する流れ

Reactの画面更新は、ざっくり次の3段階で進みます。

ステップ 内容
1 レンダリングのきっかけが発生する
2 Reactがコンポーネントを実行してUIを計算する
3 必要な変更だけをDOMに反映する(コミット)

順番に確認していきます。

2-1. レンダリングのきっかけが発生する

レンダリングが始まるきっかけは、大きく分けて次の3つです。

  • 初回表示

createRoot().render() が呼ばれたとき。アプリが最初に画面描画されるタイミングです。

  • stateが更新されたとき

もっとも基本的なパターンです。

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

setCount が呼ばれると、Reactは新しい値でUIを再計算します。useState は「値を保持すること」と「再レンダリングを起こすこと」の両方を担っています。

  • contextの値が更新されたとき

useContext でcontextの値を使っているコンポーネントは、その値が変わると再レンダリングされます。propsのバケツリレーを介さずに値を受け取れる仕組みですが、裏を返すと、contextの値が変わればそれを使っているすべてのコンポーネントが再レンダリングされる点には注意が必要です。

※補足
親が再レンダリングされると、原則として子コンポーネントも再レンダリングされます。
子に渡しているpropsの値が変わっていなくても再レンダリングされます。これは独立したトリガーというよりは、親のstateやcontextの更新が伝播した結果なので、ここでの延長として理解した方がスッキリします。スキップしたい場合は React.memo を使えますが、最適化は必要になってから行うのが基本です。

2-2. コンポーネントが実行される

きっかけが発生すると、Reactは対象のコンポーネント関数を再実行します。
関数コンポーネントは「stateやpropsを受け取って、どんなUIにするかを返す関数」です。再レンダリングとは、この関数がもう一度呼ばれて、今回表示したいUIを返すことに他なりません。

2-3. 必要な変更だけがDOMに反映される(コミット)

新しいUIが計算されると、Reactは前回の結果と比較し、変更が必要な箇所だけを実際のDOMへ反映します。これがコミットです。

2-4. コード例で流れを確認する

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

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

ボタンを押すと、次の流れが起きます。

  1. setCount(count + 1) でstateの更新が予約される(きっかけ)
  2. Reactが Counter 関数を再実行する(レンダリング)
  3. 新しいJSXが返される
  4. 前回との差分(<p> のテキスト部分だけ)がDOMに反映される(コミット)

3. バッチング — setStateを連続で呼んでも1回のレンダリングにまとまる

レンダリングの仕組みを理解するうえで、もう1つ押さえておきたいのがバッチングです。

function handleClick() {
  setCount(count + 1);
  setFlag(true);
  setText("updated");
}

このように setState を連続で呼んだ場合、Reactは3回レンダリングするのではなく、1回のレンダリングにまとめます。これがバッチングです。

React 18以降では、イベントハンドラに限らず、setTimeoutPromise の中でも自動的にバッチングされます。

バッチングを知らないと「stateを3回更新したのに途中の値が反映されていない」といった混乱が起きやすくなるので、レンダリングの仕組みとセットで押さえておくと良いです。

4. よくある誤解

4-1. 「レンダリング=DOM更新」だと思っている

これはかなり多い誤解です。

レンダリングはあくまで次のUIを計算する段階。実際にDOMへ反映されるのは、その後のコミットです。この区別ができていないと、「レンダリングが多い=パフォーマンスが悪い」と短絡的に考えてしまいがちです。

4-2. 再レンダリングを「とにかく減らすべき悪いもの」だと思っている

関数コンポーネントが再実行されること自体は、Reactの設計として自然な動作です。再実行されても、コミットのフェーズで差分だけが反映されるので、関数の再実行=パフォーマンス問題ではありません。

「再レンダリングをゼロにすること」を目的にして React.memouseMemo を乱用すると、かえってコードが複雑になり、メモ化自体のコストも発生します。最適化は、実際にパフォーマンスの問題が計測されてから行うのが基本です。

まとめ

  • レンダリングは、Reactが次のUIを計算すること(DOM更新ではない)
  • 実際のDOM更新は、レンダリング後のコミットで差分だけ反映される
  • state などの更新が再レンダリングのトリガーになる
  • 複数の setStateバッチングで1回のレンダリングにまとめられる

前々回の state、前回の ref、そして今回のレンダリング。この3つを順に追ってくると、「どの値が画面更新に関わり、どの値がそうではないのか」「Reactは何をきっかけに、何をしているのか」 が一本の線でつながってくるはずです。


参考リンク

31
10
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
31
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?