前回と前々回で、Reactにおける state と ref について整理してきました。
-
stateは、画面に反映したい値を持つもの -
refは、再レンダリングを発生させずに値やDOMを保持するもの
この2つを整理すると、 「そもそもReactのレンダリングとは何をしているのか」 という点が気になります。
そのため今回は、Reactのレンダリングについて、整理しようと思います。
1. レンダリングとは何か
Reactにおけるレンダリングとは、Reactがコンポーネントを実行し、次にどのようなUIを表示するかを計算することです。
ここで最初に押さえておきたいのは、レンダリングと実際の画面更新は同じ意味ではないという点です。
Reactの更新は大きく2つのフェーズに分かれます。
- レンダリング — コンポーネントを実行して、次に表示したいUIを計算する
- コミット — 計算結果をもとに、必要な変更を実際の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>
);
}
ボタンを押すと、次の流れが起きます。
-
setCount(count + 1)でstateの更新が予約される(きっかけ) - Reactが
Counter関数を再実行する(レンダリング) - 新しいJSXが返される
- 前回との差分(
<p>のテキスト部分だけ)がDOMに反映される(コミット)
3. バッチング — setStateを連続で呼んでも1回のレンダリングにまとまる
レンダリングの仕組みを理解するうえで、もう1つ押さえておきたいのがバッチングです。
function handleClick() {
setCount(count + 1);
setFlag(true);
setText("updated");
}
このように setState を連続で呼んだ場合、Reactは3回レンダリングするのではなく、1回のレンダリングにまとめます。これがバッチングです。
React 18以降では、イベントハンドラに限らず、setTimeout や Promise の中でも自動的にバッチングされます。
バッチングを知らないと「stateを3回更新したのに途中の値が反映されていない」といった混乱が起きやすくなるので、レンダリングの仕組みとセットで押さえておくと良いです。
4. よくある誤解
4-1. 「レンダリング=DOM更新」だと思っている
これはかなり多い誤解です。
レンダリングはあくまで次のUIを計算する段階。実際にDOMへ反映されるのは、その後のコミットです。この区別ができていないと、「レンダリングが多い=パフォーマンスが悪い」と短絡的に考えてしまいがちです。
4-2. 再レンダリングを「とにかく減らすべき悪いもの」だと思っている
関数コンポーネントが再実行されること自体は、Reactの設計として自然な動作です。再実行されても、コミットのフェーズで差分だけが反映されるので、関数の再実行=パフォーマンス問題ではありません。
「再レンダリングをゼロにすること」を目的にして React.memo や useMemo を乱用すると、かえってコードが複雑になり、メモ化自体のコストも発生します。最適化は、実際にパフォーマンスの問題が計測されてから行うのが基本です。
まとめ
- レンダリングは、Reactが次のUIを計算すること(DOM更新ではない)
- 実際のDOM更新は、レンダリング後のコミットで差分だけ反映される
-
stateなどの更新が再レンダリングのトリガーになる - 複数の
setStateはバッチングで1回のレンダリングにまとめられる
前々回の state、前回の ref、そして今回のレンダリング。この3つを順に追ってくると、「どの値が画面更新に関わり、どの値がそうではないのか」「Reactは何をきっかけに、何をしているのか」 が一本の線でつながってくるはずです。
参考リンク