Skip to main content

root.render()の動作原理

React を使用して UI をレンダーする際の最初のステップは以下の通りです:

  • createRootで root オブジェクトを作成
  • root.render(ui)関数を呼び出し
import { App } from "./app";
import { createRoot } from "react-dom/client";

const container = document.getElementById("root");

// これが最初のステップ
const root = createRoot(container);

// 次に2番目のステップ
root.render(<App />);

本セクションではroot.render関数(2 番目のステップ)に焦点を当て、そのシグネチャと内部動作を解説します。

定義

宣言

Fiber root のrenderメソッドはこちらで宣言されています。

シグネチャ

renderメソッドは以下のように定義されます:

function render(children: ReactNodeList): void {
// [Not Native Code]
}

私たちがこのパラメータをappuiと呼びがちなのとは対照的に、React のコード内ではchildrenとして参照されます。ここではchildrenという呼称を使用しましょう 😉

このパラメータの型はReactNodeListで、定義はこちらにあります:

type ReactNodeList = ReactEmpty | React$Node;

// where:
type ReactEmpty = null | void | boolean;

// and
type React$Node =
| null
| boolean
| number
| string
| React$Element<any>
| React$Portal
| Iterable<React$Node>;

// where
type React$Element<ElementType extends React$ElementType> = {
ref: any;
type: ElementType;
key: React$Key | null;
props: React$ElementProps<ElementType>;
};

上記の定義から、renderメソッドには以下のような様々な要素を渡すことが可能です(サンプルコードや実際のアプリケーションで使用される複雑な構造を含む):

import React, { createElement } from "react";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root1")).render([
"Hello ",
<span key="world" style={{ color: "red" }}>
World!
</span>,
]);

class ClassComponent extends React.Component {
render() {
const { initialCount } = this.props;

return <p>Class Count is: {initialCount}</p>;
}
}

createRoot(document.getElementById("root2")).render([
<ul key="list">
<li>First item</li>
<li>Second</li>
<li>Last, not third</li>
</ul>,
createElement(
function FunctionComponent({ initialCount }) {
return <span>Function Count is: {initialCount}</span>;
},
{ initialCount: 2, key: "count" }
),
<ClassComponent key="class" initialCount={3} />,
]);

createRoot(document.getElementById("root3")).render([
null,
true,
false,
undefined,
]);

要約すると、React Element またはそのコレクションを渡すことになります。React はこれらを再帰的にレンダーし、インタラクティブな UI を表示します。

実装

上記の実装リンクをクリックした場合にお気づきかもしれませんが、renderメソッドは以下のようになっています:

// simplified
ReactDOMRoot.prototype.render = function render(children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error("Cannot update an unmounted root.");
}

// __DEV__ only checks

updateContainer(children, root, null, null);
};

この関数は人間が理解しやすい言葉で説明すると以下の処理を行います:

  1. root._internalRoot(FiberRootNode)が null の場合(root.unmountが呼び出されたか手動で行われた場合)にエラーをスロー
  2. __DEV__環境向けのチェックと警告を実行:
    1. レガシーなReactDOM.render(children, callback)のように第 2 引数に関数を渡した場合
    2. 第 2 引数に children を渡した場合(レガシーなシグネチャ使用と判断)
    3. 第 2 引数に何かしら値を渡した場合
  3. updateContainer(children, root, null, null)を呼び出し

updateContainer

updateContainerは React コードベースの多くの箇所から呼び出される関数で、なぜ「レンダー」や「マウント」ではなく「アップデート」という名前なのか疑問に思うかもしれません。これは React が常にツリーを更新中であると扱うためです。React はツリーのどの部分が初回マウントであるかを認識し、必要に応じて適切なコードを実行します。このシリーズの後半で詳しく説明します。

この関数の実装を分析することが重要です:

シグネチャ

export function updateContainer(
element: ReactNodeList, // children
container: OpaqueRoot, // OpaqueRoot = FiberRoot = new FiberRootNode
parentComponent?: React$Component<any, any>,
callback?: Function
): Lane {
// [Not Native Code]
}

この関数は多くの処理を実行し、ツリーの初回マウント時とその後のアップデート時に使用されます。

最後の 2 つのパラメータはroot.renderから呼び出される際にnullとして渡されます。これらは使用されないことを意味します。必要な場合にのみ説明します。

ここからはupdateContainerのステップを簡略化したバージョンで説明していきます。

const current = container.current;
const lane = requestUpdateLane(current);
const update = createUpdate(lane);
update.payload = {element};
update.callback = callback;
const root = enqueueUpdate(current, update, lane);
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);

1. 現在のFiberへの参照

この関数に渡されるcontainerは、createRootに渡した DOM 要素ではありません。 これはroot._internalRoot(FiberRootNode 型)です。

前章で学んだように、container.currentプロパティはFiberNode型です。これがアプリケーションで作成された最初の Fiber です。

React はこの Fiber を参照するため、currentfiberまたはfiberNodeを意味します。

const current = container.current;

2. アップデート用レーンの要求

次に React は、現在の Fiber に対するアップデート用レーン(数値)を要求します:

const lane = requestUpdateLane(current);

これが私たちが初めて遭遇する本物の「Lanes」です。理解を容易にするため、ビット演算と 2 進数表現についての知識が必要です。

Laneは 2 の累乗数(1, 2, 4, 8, 16, 32...)で、2 進数表現で 1 つの有効ビット(1)のみを持つ整数です。React のコードベースではここで定義されています。

主なレーンとして以下が使用されます:

  • SyncLane
  • InputContinuousLane
  • IdleLane
  • OffscreenLane など
// Reactのコードベースより
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;

export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;

export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;

export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;

export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;

異なるレーンを組み合わせることで、新しい整数値(最大 32 状態)を生成します。適切なビットマスクを使用することで、複数のレーンを 1 つの数値に結合し、React が機能や挙動を検出・制御できるようにします。

React における複数レーンの組み合わせを「Lanes」と呼びます。

// Reactのコードベースより
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;

// 個人コメント:これはLanesであるべき? 不明
export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;

const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;

The requestUpdateLaneは、fiber.modeFiberNodeから取得)や他の変数を使用して必要なアップデートレーンを推論します。この関数は初期レンダー後にも実行時に呼び出されるため、そのまま説明します:

  • モードが Concurrent でない場合((mode & ConcurrentMode) === NoMode)、SyncLane2)が返されます

  • レンダーフェーズアップデート(レンダー中にsetStateを呼び出す)の場合、最優先レーンが返されます

    技術的にはlane & -laneで、数値 n に対してn & -n = 2^k(k は n の 2 進表現で最右ビットの位置)となります。今後はこれをhighestPriorityLaneと呼びます(または、与えられたLanes数値内で最小のLane😉)。

    React のLaneは賢く順序付けされています:

    // 例:任意のLanes数値
    // 0b000_1011_0000
    // 最優先レーンは0b000_0001_0000

    したがって、レンダーフェーズアップデートからコンテナを更新する場合、React は最優先レーンを採用します

  • アップデートがTransition内で発生する場合、ここで定義された TransitionLanesから選択し、次に使用するトランジションレーンをアップグレードまたはリセットします。root.render()からトランジションを強制するには、startTransitionでラップします:

    React.startTransition(() => {
    root.render(children);
    });
  • グローバルなcurrentUpdatePriorityが設定されていてNoLane0)でない場合、それが返されます

  • 上記の条件に該当しない場合、React はアップデートが React 外から発生したと判断し、Host環境にgetCurrentEventPriority()を要求します。DOM 環境の場合、window.eventを使用して優先度を推論します

3. サブツリーコンテキストの解決とアタッチ

次に React は、container(FiberRootNode).contextが null の場合、推論してアタッチします。すでに定義されている場合はcontainer.pendingContextにアタッチします。この context プロパティについては後述します

4. updateオブジェクトの作成

前章で学んだように、FiberNodeにはpendingアップデートを収集するUpdateQueueがあります。ここで最初の本物のUpdateオブジェクトが作成されます

// クロージャ変数:
// - element: root.renderに渡されたReactノード(childrenまたはui)
// - callback: updateContainerの最後のパラメータ(root.renderからはnull)

// 簡略化版
const update = {
lane,
tag: UpdateState, // 0

callback, // callbackまたはnull
payload: { element }, // elementはルートの子要素

next: null,
};

5. 作成したアップデートをFiberにエンキュー

この時点で、updateLaneを認識し、UI をペイロードとして含むFiberRootに適用するUpdateを作成しましたが、すぐには適用されません。React はこのアップデートの処理を適切にスケジュールする必要があります。

そのため、最初のステップとしてこのアップデートをキューに追加します:

const root: FiberRoot | null = enqueueUpdate(current, update, lane);

// current: FiberNode
// update: Update
// lane: number (update lane)

enqueueUpdateは以下のステップを経由します:

  1. fiber.updateQueueが null の場合、nullを返します。このパスからのみnullが返され、このfiberアンマウントされたことを意味します

  2. 開発環境でネストされたsetState呼び出しについて警告します:同じsetState(prev => next)内からsetStateを呼び出す場合。この警告はクラスコンポーネントのみに表示され、関数コンポーネントでは最新のsetStateが優先されます。両方のケースを示すCodeSandbox 例があります。

    setState内でのsetState呼び出し警告
    このスニペットは開発環境で警告を表示する方法と、hooksのsetStateからは警告が発生しないことを示しています。
    // このコードはデモ用です

    let instance;
    class ClassComponent extends React.Component {
    state = { value: "" };
    render() {
    instance = this;
    return this.state.value;
    }
    }

    let setState;
    function FunctionComponent() {
    let [state, _setState] = React.useState("");
    setState = _setState;
    return state;
    }

    function App() {
    React.useEffect(() => {
    instance.setState(() => {
    console.log("クラスコンポーネントのstateを設定中");
    // この呼び出しは開発環境で警告を発生させ、毎回setStateと見なします
    instance.setState({ value: "Hello !" });
    return { value: "この値は無視されます" };
    });
    setState(() => {
    console.log("関数コンポーネントのstateを設定中");
    // ここで返されるstateは不安定です。StrictModeが有効かどうかで異なる値が出力されます
    // index.jsでunstable_strictMode: falseに変更すると動作が変わります
    // ここでも警告が必要です
    setState("Another value");
    return "World !";
    });
    }, []);

    return (
    <div>
    <ClassComponent />
    <FunctionComponent />
    </div>
    );
    }
  3. アップデートがレンダーフェーズのクラスコンポーネントアップデート(関数コンポーネントのuseStateuseReducerフックではない)の場合:

    1. このアップデートを循環キューfiber.updateQueue.shared.pendingに追加します: 既に保留中のアップデートがある場合、新しいアップデートを先頭に配置し、既存の保留中アップデートを参照します。ここで確認できます。 最大 2 要素のpendingキューを処理する際、それらを切り離して2 番目から処理を開始します

    2. ツリーを上方向に走査し、このツリーのFiberRootNodeを検索します:

      return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
      1. getRootForUpdatedFiber(fiber)で最初の走査を行い、fiber.returnnullHostRootタグを持つ Fiber を検索します。走査中、React はネストされたアップデートをカウントし、異常を検出するとエラーをスローします

      2. markUpdateLaneFromFiberToRoot(root, null, lane)による 2 回目の走査では、HostRootに到達するまで、遭遇するすべての親にupdateLaneを追加します。OffscreenComponentsの特別なケースについては後述します。

  4. アップデートがクラスコンポーネントのレンダーではない場合(root.render()からのデフォルト分岐)、enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane)を返します:

    // 定義(簡略化)
    export function enqueueConcurrentClassUpdate<State>(): // ... パラメータ
    FiberRoot | null {
    // {element}をペイロードとするアップデートがここでキューに追加されます
    enqueueUpdate(fiber, sharedQueue, update, lane);
    // 前出のunsafeレンダーフェーズクラスコンポーネントアップデートで使用した
    // getRootForUpdatedFiberが再び使用されます
    return getRootForUpdatedFiber(fiber);
    }

    今回のenqueueUpdate以下の処理を実行します(レンダリングに関するコメントは今は無視してください):

    1. グローバルなconcurrentQueuesに 4 つの引数をキャプチャ:

      // 簡略化
      concurrentQueues[id++] = fiber;
      concurrentQueues[id++] = sharedQueue;
      concurrentQueues[id++] = update;
      concurrentQueues[id++] = lane;

      このグローバル変数は React の特定の場所でリセットされます。

    2. updateLaneをグローバルなconcurrentlyUpdatedLanesに追加:

      // 簡略化
      concurrentlyUpdatedLanes |= lane;
    3. レーンをfiberfiberRoot.current)とそのalternateにマージ:

      fiber.lanes = mergeLanes(fiber.lanes, lane);
      const alternate = fiber.alternate;
      if (alternate !== null) {
      alternate.lanes = mergeLanes(alternate.lanes, lane);
      }

    最終的に、getRootForUpdatedFiber(fiber)を通じてHostRootが返されます。

note

いずれの方法でも、FiberRootNode型のHostRootを取得します。enqueueUpdate(currentFiber, update, lane)の呼び出しはツリーのHostRootを返します。

ここで簡単に復習しましょう:

root.render(children)から始まり、以下の処理を行いました:

  • fiberRoot.current(現時点で最初に作成された Fiber)を使用
  • Transition や root のmodeなど、多くの要因に依存するアップデートレーンを要求
  • トップレベルの React contextオブジェクトを解決
  • Updateオブジェクトを作成
  • このupdateFiberupdateQueueにエンキュー

最後のステップでHostRootFiberNodeプロトタイプ)を取得しました。続きを見ていきましょう。

6. 現在のFiberアップデートをスケジュール

ここまでで、現在のfiberchildrenをペイロードとするupdateQueueconcurrentQueues配列内の変数を持っています。次に、この Fiber のアップデートをスケジュールします:

scheduleUpdateOnFiber(fiberRoot, currentFiber, updateLane);

scheduleUpdateOnFiber関数は React の多くの場所から呼び出され、レンダーをトリガーする方法です。ステートセッターやフックなどから呼び出されます。

この関数は後ほど何度も登場するため、ここで概要を説明します。現在関係ないコードパスは省略します。

関数の詳細を見ていきましょう

  1. 開発環境でinsertion effects実行中のアップデートスケジュールを警告

  2. rootin progressかつSuspendedOnData状態の場合、React が使用するグローバル変数をリセットroot.render(children)のケースでは関係ないため、後述します

  3. rootを更新済みとしてマーク: updateLanerootpendingLanesプロパティに追加します。これ保留中の作業を表します。

    このアップデートが Idle でない場合(root.render(children)からのケース) root から 2 つのプロパティ(suspendedLanes と pingedLanes)をリセットします。これは、このアップデートがツリーのブロックを解除したりサスペンド状態を解除したりする可能性があるためです。 これらのプロパティをクリアすることで、ツリーが再レンダーを試行できるようにするためです。

    export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
    root.pendingLanes |= updateLane;

    if (updateLane !== IdleLane) {
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
    }
    }
  4. アップデートがrender phase updateの場合、他のコンポーネントからの更新を警告

  5. 通常のアップデートの場合:

    1. ensureRootIsScheduled(root)を呼び出し:
      1. グローバル変数firstScheduledRootlastScheduledRootに root を登録
      2. マイクロタスクをスケジュール:scheduleMicrotaskを使用してprocessRootScheduleInMicrotaskを実行。scheduledRootsをループ処理します:
        scheduleImmediateTask(processRootScheduleInMicrotask);
        処理はスケジュールのみされ、実行は後ほど詳細を説明します
    2. rootLegacyの場合、即座にアップデートをフラッシュ

7. Fiberのトランジションを entangle

このセクションの最後のステップです。ここまで到達したあなたは好奇心と野心の持ち主です。長く複雑なセクションでしたが、これが最後の部分です。

初回レンダーが基本的な場合(例:シンプルなroot.render()):

createRoot(container).render(children);

この場合、この関数では何も行われません。

しかしstartTransitionrenderをラップする場合:

function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
const sharedQueue = fiber.updateQueue.shared;

if (isTransition(lane)) {
let queueLanes = sharedQueue.lanes;

queueLanes &= root.pendignLanes;

const newLanes = queueLanes | lane;
sharedQueue.lanes = newQueueLanes;

markRootEntangled(root, newLanes);
}
}
  • fibershared.lanesrootpendingLanesと交差します。これにより両方に存在する共通のレーンのみが残ります

  • 次に、このケースTransitionLaneを含むupdateLaneとマージされ、fiber.updateQueue.shared.lanesに割り当てられます

  • 最後のステップはmarkRootEntangled(root, newQueueLanes)の呼び出しです。複雑なプロセスのため、段階的に説明します:

    1. newQueueLanesroot.entangledLanesに追加

    2. ループ前にroot.entanglements配列を参照

    3. lanes変数として参照し、lanes0でない間ループ:

      1. 現在のlanesインデックスを計算: インデックスは31から現在のlanes数値の先頭のゼロの数を引いた値。これは現在のlanesの 2 進表現で最初の有効ビット(1)の位置です

      2. lanes数値が複数のレーンの組み合わせの場合、最上位ビットのlaneは取得したインデックス分左シフトして計算:

        const lane = 1 << index;

        // 例:
        // lanes = 21(0b0000_0000_0000_0000_0000_0000_0001_0101)
        // clz32(lanes) = 27
        // 31 - clz32(lanes) = 4
        // 1 << 4 = 0b0000_0000_0000_0000_0000_0000_0001_0000(16)
        // これは最優先度が低いレーン(数値が最も高い)を示す
      3. レーンがnewQueueLanesに存在し、newQueueLanesと推移的に関連している場合、newQueueLanesレーンの entanglement(root.entanglements) に追加

        // very simplified
        // non-zero will be equal to the lane itself
        const laneOrZero = lane & newQueueLanes;

        // the existing entanglements at index
        const entagledLanesAtIndex = entanglements[index];
        // lanes that were entangled intersecting with new queue lanes
        // those are lanes that were present already and are coming again
        const persistingLanesAtIndex = entagledLanesAtIndex & newQueueLanes;

        // this means that either this lane is directly present in the new lanes
        // or that it is transitively present from the previous entanglements
        if (laneOrZero | persistingLanesAtIndex) {
        // add the new lanes to the existing entanglements
        entanglements[index] |= newQueueLanes;
        }
      4. 現在のlanelanesから削除し、lanes0になるまでループ継続

これでupdateContainer(children, root, null, null)が終了します。

まとめ

root.render(children)の主な目的は、updateContainer関数を呼び出し、アップデートオブジェクトを作成してroot._internalRoot.current.shared.pendingにキューイングすることです。この際、childrenelementとしてペイロードに保持します。

この処理中、React はアップデートの発生源について多くのチェックを行います。root.render()からのケースではほとんどのチェックが該当しませんが、知っておくことが重要です。

現時点で最も重要な部分はscheduleImmediateTask(processRootScheduleInMicrotask)です。これは後で実行されるコードをスケジュールしますが、まだ詳細は説明していません。これには理由があります:ワークループが開始されるからです。

note

createRoot(container, options)root.render(children)の内部動作を見てきましたが、React はまだコンポーネントのレンダーを開始していません。queueMicrotaskを通じて作業をスケジュールしただけです。

これは、React があなたのコンポーネントをindex.jsファイルのスクリプトが完了した後にレンダーすることを意味します 😉。

root.render(children);
triggerImportantDataFetch();
RegisterServiceWorker();

この機会を利用して、エフェクトなどを経由せずに重要なデータロードを開始し、初期状態でpending状態に入ったりサスペンドしたりできます。例えば現在のユーザーの解決を開始できます。