Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
React inside basics: learn from “build own react"
Search
ryounasso
June 21, 2025
Programming
240
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
React inside basics: learn from “build own react"
ryounasso
June 21, 2025
More Decks by ryounasso
See All by ryounasso
明日から始めるリファクタリング
ryounasso
0
290
駆け足で Google から学ぶテスト設計の指針
ryounasso
0
230
抽象データ型について学んだ
ryounasso
0
470
開発効率向上のためのリファクタリングの一歩目の選択肢 ~コード分割~ / JJUG CCC 2024 Fall
ryounasso
0
4.3k
Clean Architecture by TypeScript & NestJS
ryounasso
0
1.2k
Fast API を用いた Web API の開発
ryounasso
1
640
テストゼロの個人開発プロジェクトにテストを導入した話
ryounasso
0
520
簡易 DI コンテナを作って DI コンテナを知る
ryounasso
1
1.4k
TypeScript_コンパイラの内側に片足を入れる
ryounasso
3
1.1k
Other Decks in Programming
See All in Programming
市販E-Readerを乗っ取れ 〜Embedded Swiftで電子ペーパーガジェットを制御する〜
trickart
0
160
iOS開発×AI駆動開発 〜最近使って便利だったスキルの話〜
nogu66
0
140
20260828_品質と開発生産性を両立させる、AI時代のE2Eテストの考え方
magicpod
0
200
Swift愛好会と私(ウホーイ) / Swift Fan Club and Uhooi
uhooi
0
150
GraphRAGのKnowledge Graphを 直接!見る/View-GraphRAG's-KnowledgeGraph-directly!
tyumugi1113
1
290
MIZARU@SPAJAM2026 第二回予選
1901drama
0
110
Security issues being discussed on Web Platforms
petamoriken
0
760
【DroidKaigi 2026】「アクセシビリティを利用するとき、 アクセシビリティもまたこちらを利用している」 〜マルウェアによる攻撃と防衛について〜
halunoyo
0
450
tsc.rip を支える技術 / Kyoto.なんか #8
susisu
0
4.4k
XHTMLが残したもの
yosuke_furukawa
PRO
2
700
RSSとCodexを使ってX投稿自動化してみた
ochtum
0
110
AI Agent時代のリアーキテクチャ戦略と実践
hokaccha
9
3.9k
Featured
See All Featured
What's in a price? How to price your products and services
michaelherold
247
13k
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.2k
Believing is Seeing
oripsolob
1
210
Six Lessons from altMBA
skipperchong
29
4.5k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
500
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
エンジニアに許された特別な時間の終わり
watany
108
250k
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
2
3.8k
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.7k
How to make the Groovebox
asonas
2
2.4k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
Git: the NoSQL Database
bkeepers
PRO
432
67k
Transcript
React inside basics: learn from “build own react” 1
Build own React というブログ記事の内容に取り組んでみました。 この記事では実際の React のアーキテクチャに沿って、React を 1 から書いていきま
す。 最適化機能を実装していなかったり、完全に再現しているわけではありません。 取り組む中で React の内部実装の基礎知識を得ることができたので、その内容につい てお話しします。 関数型コンポーネントを前提としています。 2
目次 React の内部の基礎知識 i. 仮想 DOM ii. 作業単位の分割とスケジューリング iii. レンダーとコミットの分離
iv. Reconciliation v. hooks 簡易的な useEffect を実装してみる 3
React の内部の基礎知識 4
1. 仮想 DOM React の基本的な考え方 → 状態が変わるごとにコンポーネントを毎回実行して DOM を新規に構築 ただ、実
DOM を操作すると、パフォーマンス面が厳しい。 → そこで 仮想 DOM 仮想 DOM は JavaScript オブジェクトの木構造 実際は仮想 DOM という単語は使用しないとのこと (便宜上今回は仮想 DOM と呼ぶ) 以下の記事で詳しく解説されている 「仮想DOM」という用語を使わない 5
仮想 DOM の例 { type: "div", props: { children: [
{ type: "p", props: { children: ["Count: ", state] } }, { type: "button", props: { onClick: () => setState((c) => c + 1), children: "Count up" } } ] } } 6
const container = document.getElementById("root") // element: 仮想 DOM オブジェクト Didact.render(element,
container) function render(element, container) { // element.type に合わせて実 DOM 要素を生成 const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type) const isProperty = key => key !== "children" Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] }) element.props.children.forEach(child => render(child, dom) ) // 生成した DOM を追加 container.appendChild(dom) } 仮想 DOM オブジェクトの内容に沿って実 DOM を生成してレンダリングする 7
2. 作業単位の分割とスケジューリング 大きなコンポーネントだとツリーが大きくなる → コンポーネントのレンダリング中スレッドを占有してしまう 解決策: レンダリング作業を小さな単位に分割 → Fiber 特徴:
各Fiberは1つの作業単位(要素)を表現 親・子・兄弟の参照を持つ連結リスト alternate プロパティで前回のレンダリング結果を保持 処理の分割により Fiber のスケジューリングが可能 優先度の低い Fiber はブラウザのアイドル時間に処理 優先度の高い Fiber は早いタイミングで実行 8
https://pomb.us/build-your-own-react/ 9
{ type: "div", props: { children: [pFiber, buttonFiber] }, dom:
div, // 実際の DOM 要素への参照 parent: counterFiber, // 親 Fiber child: pFiber, // 第一の子要素の Fiber sibling: null, // 同じ親を持つ要素の Fiber alternate: null, // 前回のレンダリング結果を保持する Fiber effectTag: "UPDATE" // DOM 操作のタイプ } // button Fiber { type: "button", props: { onClick: () => setState((c) => c + 1), children: ["Count up"] }, dom: button, // 実際の DOM 要素への参照 parent: divFiber, child: buttonTextFiber, // ボタンテキストの Fiber sibling: null, alternate: oldButtonFiber, effectTag: "UPDATE" } 10
3. レンダーとコミットの分離 Fiber によって優先度の高い処理を優先的に行うことが可能になった 例:ユーザーの入力処理の割り込みが入る レンダリング途中で DOM を更新すると不完全な UI が表示される
→「レンダー」と「コミット」の2フェーズに分離 レンダーフェーズ: 仮想 DOM の差分計算を行う(中断可能) DOM 操作は一切行わない コミットフェーズ: 差分計算完了後に実行(中断不可) まとめて DOM 操作を実行 11
登場するグローバル変数の説明 nextUnitOfWork : 次に処理すべき Fiber wipRoot : 作業中の Fiber のルート
currentRoot : 前回コミットされた Fiber ツリー deletions : 削除すべき Fiber のリスト 12
レンダリングフェーズ function workLoop(deadline) { // shouldYield: ブラウザにレンダリング制御を戻すべきかを示すフラグ while (nextUnitOfWork &&
!shouldYield) { // performUnitOfWork: 1つの Fiber の処理と次の Fiberの取得 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 残された時間が1ms未満になったら中断。高優先度タスク(入力処理やアニメーション)に道を譲る shouldYield = deadline.timeRemaining() < 1; } // === レンダーフェーズ完了時の処理 === if (!nextUnitOfWork && wipRoot) { // レンダーフェーズで計算した変更を実 DOM に適用 commitRoot(); } } 13
コミットフェーズ function commitRoot() { // deletions: 削除すべきノードのリスト // 削除操作を最初に行う deletions.forEach(commitWork);
// wipRoot.child: ルートファイバーの子から始めて再帰的に全ての変更をコミット commitWork(wipRoot.child); // 次回の差分計算のために現在のルートを完了したルートで更新 currentRoot = wipRoot; // currentRoot: 前回コミットされたファイバーツリー // 作業用ルートをクリア(コミット完了の印) wipRoot = null; } 14
コミット作業を行う。すべてのノードを DOM に再帰的に追加する。 function commitWork(fiber) { if (!fiber) { return;
} let domParentFiber = fiber.parent; while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom); } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); } else if (fiber.effectTag === "DELETION") { commitDeletion(fiber, domParent); } commitWork(fiber.child); commitWork(fiber.sibling); } 15
4. Reconciliation DOM にレンダリングしたい内容と、前回のレンダリングの内容を比較して差分を検知 する 比較した結果、DOM に適用すべき変更があるかどうかを確認する 適用すべき変更内容の確認方法 古い Fiber
と新しい Fiber の要素が同じタイプ = 更新 古い Fiber と新しい Fiber のタイプが異なり、新しい要素がある = 新規追加 古い Fiber と新しい Fiber のタイプが異なり、古い Fiber がある = 削除 16
Reconciliation の処理 // elements: 新しくレンダリングしたい要素の配列 function reconcileChildren(wipFiber, elements) { let
oldFiber = wipFiber.alternate?.child; // 新しい Fiber と今の Fiber を比較する for (let i = 0; i < elements.length || oldFiber != null; i++) { const element = elements[i]; const sameType = oldFiber && element && element.type == oldFiber.type; if (sameType) { // 更新: 同じ型の場合はDOMノードを保持し、プロパティのみ更新 // effectTag: "UPDATE" } else if (element) { // 新規作成: 新しい要素がある場合 // effectTag: "PLACEMENT" } else if (oldFiber) { // 削除: 古い要素のみ存在する場合 // effectTag: "DELETION" } } } 17
例えば、カウンターが1から2に変わった場合 <!-- 古い要素 --> <p>カウント: 1</p> <!-- 新しい要素 --> <p>カウント:
2</p> 18
レンダーフェーズ reconcileChildren 関数が実行され、同じ p タグ同士であるため、 sameType が true となる 後のコミットフェーズでの処理のために
p の Fiber の alternate や effectTag を 更新する if (sameType) { // update the node newFiber = { // ... alternate: oldFiber, effectTag: "UPDATE", }; } 19
コミットフェーズ commitRoot 関数が実行され、 p の effectTag が "UPDATE" なので updateDom
関 数が実行される if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); // テキストノードの内容が"カウント: 2"に更新される } これにより、変更内容が DOM に反映されてユーザーにはカウンターの値が 2 と表示さ れる 20
5. hooks React が提供する API 状態を管理 useState , useReducer 作用を管理
useEffect , useLayoutEffect メモ化 useMemo , useCallback 参考 React https://speakerdeck.com/recruitengineers/react-2023?slide=108 21
useState 状態を扱うためのフック 特徴 関数コンポーネントで状態を維持することが可能になる 各コンポーネントの Fiber に配列として保存される 初期値を引数に取り、現在の状態と状態更新関数を返す setState を呼ぶと、コンポーネントの再レンダリングを発火
状態更新はすぐには適用されず、次のレンダリングサイクルで処理される 複数の useState を使って、複数の状態を管理できる 呼び出し順序に基づいてフックを識別(インデックスを使用) 22
function useState(initial) { // 前回のレンダリング時のフックを取得する。 wipFiber.alternate.hooks に前回のフックが保存されている const oldHook =
wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // フックのオブジェクトを生成。前回のフックが存在すれば前回の状態、なければ初期値を使用。queue は状態更新関数を保存する配列 const hook = { state: oldHook ? oldHook.state : initial, queue: [] }; // 前回のレンダリングから適用されていない状態更新関数を適用することで常に最新の state が返却される const actions = oldHook ? oldHook.queue : []; actions.forEach((action) => { hook.state = action(hook.state); }); // 状態を更新するための関数 const setState = (action) => { // 省略 }; // 現在の Fiber にこのフックの情報を保存。複数の state を管理するために Fiber には配列でフックを保存 wipFiber.hooks.push(hook); // 次のフックのためにインデックスを増やす。 hookIndex++; return [hook.state, setState]; } 23
setState 関数の実装 // 状態を更新するための関数 const setState = (action) => {
// 更新アクションをキューに追加。これで次回のレンダリング時に oldHook.queue から取得可能 hook.queue.push(action); // 新しい Fiber のルートを作成 wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, // 現在のツリーを前回のツリーとして参照 }; // nextUnitOfWork が次に処理すべき Fiber を格納する変数 // 新しいレンダリングフェーズを開始するために新しい wipRoot を次の作業単位として設定。 // この処理があるから、setState の呼び出しで再レンダリングが発火する nextUnitOfWork = wipRoot; deletions = []; }; 24
簡易的な useEffect を実装してみる 25
ここまで学習した内容をもとに独自の useEffect フックを実装してみます。 実際の処理を追うところまでできなかったので、こんな感じかな?という部分が多く あります 26
useEffect とは? 関数コンポーネントで「副作用」を扱うためのフック 副作用: レンダリング以外の処理(データ取得、購読、DOMの直接操作など) 依存配列とエフェクト関数、クリーンアップ関数を持つ useEffect(() => { document.title
= `クリック数: ${count}`; // クリーンアップ関数(任意) return () => { console.log('コンポーネントがアンマウントされるか再レンダリングされます'); }; }, [count]); // 依存配列 27
useEffect の実装において考えること フックの情報管理 フックを Fiber に保存して再レンダリング間で状態を維持 依存配列、エフェクト関数、クリーンアップ関数、を保持 依存配列の比較 依存配列に変更があった場合のみエフェクトを実行 初回レンダリング時は無条件で実行
エフェクトの実行タイミングの管理 DOM更新(コミットフェーズ)完了後にエフェクトを実行 副作用なので、DOM が更新された後に実行する必要がある 再レンダリング時とアンマウント時にクリーンアップ関数を実行 28
フックの情報管理 // 副作用(エフェクト)をコンポーネントのレンダリング後(コミットフェーズ後)に実行するための準備をする関数 function useEffect(effect, deps) { // 現在の Fiber
から前回のフックを取得 const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // 保存したいフック情報を作成 const hook = { effect, // エフェクト関数 deps, // 依存配列 cleanup: oldHook ? oldHook.cleanup : undefined, // クリーンアップ関数。前回の effect で返されたクリーンアップ関数を実行する必要がある fiber: wipFiber // Fiberへの参照 }; // Fiber にフック情報を保存 wipFiber.hooks.push(hook); hookIndex++; // エフェクトの実行自体は後のコミット完了後に行うため、ここではフックを保存するだけ effectHooks.push(hook); } フックを Fiber に保存して再レンダリング間で状態を維持 依存配列、エフェクト関数、クリーンアップ関数を保持 29
依存配列の比較 function areHookDepsEqual(prevDeps, nextDeps) { if (!prevDeps || !nextDeps) return
false; if (prevDeps.length !== nextDeps.length) return false; return prevDeps.every((dep, i) => Object.is(dep, nextDeps[i])); } 30
function useEffect(effect, deps) { const oldHook = wipFiber && wipFiber.alternate
&& wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; const hook = { effect, deps, cleanup: oldHook ? oldHook.cleanup : undefined, fiber: wipFiber // fiberへの参照を追加 }; // 依存配列が変更されたか、または初回実行時はエフェクトを実行キューに追加 const hasChangedDeps = oldHook ? !areHookDepsEqual(oldHook.deps, hook.deps) : true; if (hasChangedDeps) { // エフェクトの実行自体は後のコミット完了後に行うため、ここではフックを保存するだけ。`runEffects` 関数で実行される effectHooks.push(hook); } wipFiber.hooks.push(hook); hookIndex++; } 31
実行タイミングの管理 DOM更新後にエフェクトを実行 → DOMが更新されてから副作用を実行しないと、古い状態に対して操作してしまう // コミットフェーズで呼び出す関数 function commitRoot() { //
DOM への反映処理 deletions.forEach(commitWork); commitWork(wipRoot.child); // DOM更新後にエフェクトを実行 runEffects(); // 次のレンダリングサイクルの準備 currentRoot = wipRoot; wipRoot = null; } 32
エフェクトの実行と再レンダリング時のクリーンアップ処理 function runEffects() { // effectHooks をコンポーネント単位でグループ化して処理 const componentEffects =
{}; // コンポーネントごとにエフェクトをグループ化・コンポーネントIDをキーとして使用 effectHooks.forEach((hook) => { const componentId = hook.fiber.type.name || "undefined"; if (!componentEffects[componentId]) { componentEffects[componentId] = []; } componentEffects[componentId].push(hook); }); // 各コンポーネント内で前のエフェクトのクリーンアップを実行してから、新しいエフェクトを実行 Object.values(componentEffects).forEach((hooks) => { hooks.forEach((hook) => { if (hook.cleanup) { hook.cleanup(); } }); hooks.forEach((hook) => { hook.cleanup = hook.effect(); }); }); effectHooks = []; } 33
アンマウント時のクリーンアップ関数の実行 // コンポーネントがアンマウントされる際に呼び出す関数 function commitDeletion(fiber, domParent) { // コンポーネント削除前にクリーンアップ関数を実行 if
(fiber.hooks) { fiber.hooks.forEach(hook => { if (hook.cleanup) { hook.cleanup(); } }); } if (fiber.dom) { domParent.removeChild(fiber.dom); } else { commitDeletion(fiber.child, domParent); } } 34
実装した useEffect を使う function useEffect(effect, deps) { // 省略 }
export const Didact = { createElement, render, useState, useEffect }; Didact.useEffect(() => { document.title = `クリック数: ${count}`; return () => { console.log("Counter unmounted or updated"); }; }, [count]); 35
まとめ React の内部実装の基礎知識を学ぶことができた 仮想 DOM の概念 作業単位の分割とスケジューリング (Fiber) レンダーとコミットの分離 Reconciliation
の仕組み hooks の実装方法 これによって今後のアップデートの意図や仕様の理解がスムーズになる予感 36
参考資料 Build your own React React 研修 ReactはなぜFiberで書き直されたのか?Reactの課題と将来像を探る 「仮想DOM」という用語を使わない 37