React 関数コンポーネントの更新はどのようにトリガーされるのか

  • 1786単語
  • 9分
  • 21 Sep, 2024

React 関数コンポーネントは、Hooks が導入されて以来、現代の React アプリ開発の中核となっています。クラスコンポーネントに比べて関数コンポーネントはより簡潔でありながら、強力な機能を持っています。しかし、その更新メカニズム、特にソースコードの観点から理解することで、パフォーマンスを最適化し、不要な再レンダリングを避けることができます。本記事では、React の低レベルなソースコードから、関数コンポーネントの更新メカニズムを深く解析します。

1. 関数コンポーネント更新のトリガー条件

関数コンポーネントの更新は主に以下の方法でトリガーされます:

  • State の変化useState の setter メソッドで状態を更新する。
  • Props の変化:親コンポーネントが渡す props が変化すると、React はそのコンポーネントを再レンダリングします。
  • Context の変化useContext で取得したコンテキストデータが変化した際に、React は関連するコンポーネントを再レンダリングします。

ソースコードの観点では、React はコンポーネントの状態や props などのデータを内部の Fiber ツリー に保存し、それらのデータが変化した際に React は調和(Reconciliation)段階に入り、コンポーネントの再レンダリングが必要かどうかを決定します。

2. Fiber ツリーの核心的役割

React 16 で Fiber アーキテクチャが導入されて以降、すべてのコンポーネント(クラスコンポーネントや関数コンポーネント)は Fiber ノード として更新が表現されるようになりました。Fiber ツリーの役割は、更新プロセスを小さなタスクに分割して実行することにより、大きなタスクを一度に完了させる代わりに、ユーザーインターフェースの応答性を保つことです。

各関数コンポーネントには対応する Fiber オブジェクトが存在し、このオブジェクトには 状態(state)props、および 更新キュー(update queue) が記録されています。setStateuseState の setter 関数が呼ばれると、React はこの更新を Fiber ノードの更新キューに格納し、スケジューラーがそれを実行するまで待機します。

3. React の更新スケジューリングプロセス

関数コンポーネントの状態や props が変化した際、React は更新スケジューリングプロセスに入ります。主要なフローは次の通りです:

3.1 setState および useState の更新メカニズム

useState の setter メソッド(例:setState)が呼ばれると、新しい状態値と更新対象のコンポーネント参照を含む 更新オブジェクト が作成されます。この更新オブジェクトは現在の Fiber ノードの更新キューに追加され、React のスケジューリングを待ちます。

1
// 簡略化された useState 実装
2
function useState(initialState) {
3
const hook = getHook(); // 現在の Fiber ノードからフック状態を取得
4
if (!hook) {
5
// フック状態を初期化
6
return mountState(initialState);
7
}
8
return updateState(hook);
9
}

各更新ごとに、React は Fiber ツリー内の各 Fiber ノードに対して更新ロジックを実行します。beginWork 関数を通じて更新キューを確認し、状態値を再計算してコンポーネントの再レンダリングをトリガーします。

3.2 Hooks の保存と再利用

関数コンポーネントが実行されるたびに、React は内部のリストを通じて Hooks を保存および再利用します。各 useStateuseEffect の呼び出しは、このリストにフックノードを作成または再利用し、状態値や副作用を保存します。

1
function renderWithHooks(currentFiber, nextChildren) {
2
currentlyRenderingFiber = currentFiber;
3
currentHook = currentFiber.memoizedState; // 以前に保存されたフックリストを取得
4
nextChildren = Component(props); // 関数コンポーネントを再実行
5
return nextChildren;
6
}

関数コンポーネントが更新されるたびに、React は関数体を最初から再実行しますが、各 Hook は順番に保存されているため、一貫性を保ちながら対応する状態や副作用を取り出すことができます。

4. 調和アルゴリズムと仮想 DOM の動作原理

4.1 仮想 DOM の Diff アルゴリズム

React の更新メカニズムは 調和アルゴリズム(Reconciliation) に依存して、どの部分を更新するかを決定します。調和の主なステップは次の通りです:

  1. 新しい仮想 DOM の作成:コンポーネントが更新されるたびに、React は新しい状態と props に基づいて新しい仮想 DOM ツリーを生成します。
  2. Diffing 段階:React は新旧の仮想 DOM ツリーを比較し、Diff アルゴリズムによって変更が必要な部分を特定します。
  3. 実際の DOM を更新:React は差分を最小限に抑えて実際の DOM をバッチ処理で更新します。

Fiber ツリーにより、React は作業単位(更新ステップ)の間で一時停止および再開が可能になり、長いタスクの実行プロセスが最適化され、アプリケーションの応答性が向上します。

4.2 更新優先度とスケジューリング

React は 優先度キュー を使用して更新のスケジューリング順序を制御します。状態や props の変化が発生すると、React は更新タスクに優先度を割り当て、緊急度に応じて実行のタイミングを決定します。

  • 高優先度の更新(例:ユーザー入力イベント)は即座に実行されます。
  • 低優先度の更新(例:アニメーションやバックグラウンドデータの読み込み)は後回しにされ、UI の滑らかさが保たれます。

5. 関数コンポーネントのパフォーマンス最適化:useMemo と useCallback

毎回コンポーネントが更新されると、関数全体が再実行されるため、useMemouseCallback を使用して高コストな計算や関数の参照をキャッシュすることが非常に重要です。

  • useMemo:計算結果をキャッシュし、再レンダリング時に同じ計算を繰り返すのを避けます。
  • useCallback:関数をキャッシュし、子コンポーネントで毎回新しい関数参照を作成するのを避けます。
1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
2
3
const memoizedCallback = useCallback(() => {
4
handleClick(a);
5
}, [a]);

これらのHooksは、依存関係が変わらない限り値や関数をキャッシュすることで、不要な再計算を減らし、アプリのパフォーマンスを向上させます。

6. 関数コンポーネントのライフサイクルの模倣

関数コンポーネントにはクラスコンポーネントのライフサイクルメソッドはありませんが、useEffect を使用して似たようなライフサイクル機能を実現できます。

  • componentDidMount および componentDidUpdateuseEffect を使用してシミュレートし、コンポーネントがマウントまたは更新されるたびに実行されます。
  • componentWillUnmountuseEffect 内でクリーンアップ関数を返すことでシミュレートします。
1
useEffect(() => {
2
// マウントまたは更新時に実行
3
return () => {
4
// アンマウント時に実行
5
};
6
}, [dependencies]); // 依存配列で実行タイミングを制御

結論

React 関数コンポーネントの更新プロセスは、状態変化から始まり、Fiber ツリーと調和アルゴリズムを通じて仮想DOMの更新が段階的に進み、最終的に実際のDOMに反映されます。Hooksの導入により、関数コンポーネントはよりシンプルになりましたが、開発者はパフォーマンス最適化にも注意を払う必要があります。例えば、useMemouseCallback、および React.memo を適切に使用することが推奨されます。

これらの基礎的なメカニズムを理解することで、より効率的なReactアプリケーションを構築し、不要なパフォーマンスボトルネックを回避する手助けとなります。