Fiber がどのようにツリーを再調整し、更新をバッチ処理し、レンダリング作業を中断可能にするかを学びます。
このレッスンを Kodokon で開くReact 16 以降、内部エンジンは Fiber と呼ばれています。ツリー内のすべての要素は、作業単位である fiber ノードとして表現されます。React は実際には 2 つのツリー(ダブルバッファリングという手法)を保持しています。画面に表示されている内容を反映する current ツリーと、レンダリング中に構築される workInProgress ツリーです。作業は 2 つの明確に異なるフェーズで行われます。レンダーフェーズ(コンポーネントを呼び出し、差分を計算する)は中断可能で再実行可能であり、次に コミットフェーズ(DOM への変更を適用し、エフェクトを実行する)は常に 同期的かつ中断不可能 です。再調整は、比較を線形コストに抑える 2 つのヒューリスティックに依存しています。ある位置で要素の 型 が変わると、React はサブツリー全体を破棄し、state も含めて再構築します。リストの場合、key プロパティによって React は 2 回のレンダー間で要素を対応付けられます。
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<input defaultValue={item.label} />
</li>
))}
</ul>
);
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.2 つ目の基本的なメカニズム: バッチ処理。同じコンテキスト内で複数の state 更新がトリガーされると、React はそれらをまとめて 単一のレンダー にします。React 18 より前は、このグループ化は React のイベントハンドラー内でしか機能しませんでした。createRoot 以降は、タイマー、Promise、ネイティブリスナーなど あらゆる場所で自動的 に行われます。これはまた、setState の直後に state を読み取ると古い値が返る理由でもあります。更新はスケジュールされるのであって、即座に適用されるわけではないのです。
function handleTick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: a single render, even here.
}
setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.並行レンダリング はこのロジックをさらに推し進めます。より緊急な更新が入ってきた場合、レンダーフェーズは 中断、再開、あるいは破棄して再開 することができます。React は更新を優先度(lanes)で順位付けします。キー入力は緊急ですが、startTransition でマークされたリストフィルターは緊急ではありません。その直接的な結果として、あなたのコンポーネント関数は 純粋 でなければなりません。React は結果をコミットすることなく、それらを何度も呼び出す可能性があるからです。もう 1 つの特殊なケース: 中断可能なレンダー中に読み取られる外部ストア(Redux、Zustand、自作のシングルトン)は レンダーの途中で 変化する可能性があり、tearing(テアリング) として知られる一貫性のないインターフェースを生み出します。useSyncExternalStore API は、ストアが変化したときに同期レンダーを強制することで、この問題を解決します。
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(notify) => {
window.addEventListener('resize', notify);
return () =>
window.removeEventListener('resize', notify);
},
() => window.innerWidth
);
}<div> 要素が <span> に変わると React は何をしますか?createRoot を使う場合、setTimeout 内の 2 回の setState 呼び出しは何回のレンダーをトリガーしますか?