Walkthrough: First Render End to End
createRoot(el).render(<App/>) renders nothing synchronously. It builds the root objects, attaches the event listeners, queues one DefaultLane update and books a microtask. The…
- Published
- Reading time
- 18 min read
This page is the narrative. Every step links to the deep-dive page that explains it properly. The companion story, a click that updates the counter, is setState Walkthrough.
The running example
The same app is used on both walkthrough pages. I added one layout effect and one passive effect so the commit phase has something to run:
import { useState, useEffect, useLayoutEffect } from "react";
import { createRoot } from "react-dom/client";
function Header({ title }) {
return <h1>{title}</h1>;
}
function Footer() {
return <footer>made with React</footer>;
}
function App() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
document.title = `count: ${count}`;
}, [count]);
useEffect(() => {
console.log("committed", count);
}, [count]);
return (
<div>
<Header title="Counter" />
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Footer />
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);The fiber tree this produces has eight fibers: HostRoot → App → div → [Header → h1, button, Footer → footer]. There are no HostText fibers. <h1>{title}</h1>, <button>{count}</button> and <footer>made with React</footer> each have a single string or number child, and shouldSetTextContent says those are written as textContent rather than getting their own fiber.1 That detail comes back in the update walkthrough.
The whole flow
sequenceDiagram
autonumber
participant JS as your module
participant DOM as react-dom
participant RC as reconciler
participant MT as microtask
participant S as Scheduler (MessageChannel)
participant WL as work loop
participant C as commit
participant B as browser
JS->>JS: JSX → jsx() → element objects
JS->>DOM: createRoot(container)
DOM->>RC: createContainer → FiberRootNode + HostRoot fiber
DOM->>DOM: markContainerAsRoot, listenToAllSupportedEvents
JS->>DOM: root.render(<App/>)
DOM->>RC: updateContainer: requestUpdateLane = DefaultLane
RC->>RC: createUpdate {payload: {element}} → concurrentQueues
RC->>RC: scheduleUpdateOnFiber → markRootUpdated → ensureRootIsScheduled
RC->>MT: queueMicrotask (once)
Note over JS,MT: script ends, nothing rendered yet
MT->>RC: processRootScheduleInMicrotask
RC->>S: scheduleCallback(Normal, performWorkOnRootViaSchedulerTask)
S->>WL: (new macrotask) performWorkOnRoot(root, DefaultLane)
WL->>WL: shouldTimeSlice = false → renderRootSync
WL->>WL: prepareFreshStack: WIP HostRoot, flush queued update
WL->>WL: beginWork ↓ (HostRoot, App: hooks mount, div, Header, h1, …)
WL->>WL: completeWork ↑ (createInstance, appendAllChildren, bubbleProperties)
WL->>C: completeRoot → commitRoot
C->>S: scheduleCallback(Normal, flushPassiveEffects)
C->>C: before mutation: clearContainer
C->>B: mutation: container.appendChild(div) (once)
C->>C: root.current = finishedWork
C->>C: layout: useLayoutEffect, refs
C->>S: requestPaint()
S-->>B: yield → style, layout, PAINT
S->>C: (next macrotask) flushPassiveEffects → useEffectCall stack at a glance
── synchronous, in your module ───────────────────────────────────────────
jsx(App, {}) → ReactElement {type: App, props: {}}
createRoot(container)
createContainer → createFiberRoot → FiberRootNode + HostRoot fiber, initializeUpdateQueue
markContainerAsRoot(root.current, container)
listenToAllSupportedEvents(container)
root.render(<App/>) (ReactDOMRoot.prototype.render)
updateContainer
requestUpdateLane(current) → DefaultLane
updateContainerImpl
createUpdate(lane); update.payload = {element}
enqueueUpdate → enqueueConcurrentClassUpdate (buffered in concurrentQueues)
scheduleUpdateOnFiber
markRootUpdated (root.pendingLanes |= DefaultLane)
ensureRootIsScheduled → ensureScheduleIsScheduled
scheduleImmediateRootScheduleTask → queueMicrotask(...)
── microtask ──────────────────────────────────────────────────────────────
processRootScheduleInMicrotask
scheduleTaskForRootDuringMicrotask → scheduleCallback(NormalPriority, performWorkOnRootViaSchedulerTask)
── macrotask: Scheduler (MessageChannel) ──────────────────────────────────
performWorkUntilDeadline → flushWork → workLoop
performWorkOnRootViaSchedulerTask
performWorkOnRoot(root, DefaultLane, forceSync=false)
renderRootSync (shouldTimeSlice === false)
prepareFreshStack → createWorkInProgress(root.current), finishQueueingConcurrentUpdates
workLoopSync → performUnitOfWork × 8
beginWork(HostRoot) → updateHostRoot → processUpdateQueue → reconcileChildFibers
beginWork(App) → updateFunctionComponent → renderWithHooks → App()
mountState, mountLayoutEffect, mountEffect → mountChildFibers
beginWork(div) → updateHostComponent → reconcileChildrenArray
…
completeUnitOfWork → completeWork(h1 | button | footer | div)
createInstance, appendAllChildren, finalizeInitialChildren, bubbleProperties
finishConcurrentRender → completeRootWhenReady → completeRoot → commitRoot
commitBeforeMutationEffects (HostRoot Snapshot → clearContainer)
flushMutationEffects → commitMutationEffects → … → commitPlacement
insertOrAppendPlacementNodeIntoContainer → appendChildToContainer
root.current = finishedWork
flushLayoutEffects → commitLayoutEffects → commitHookLayoutEffects
flushSpawnedWork → requestPaint()
── browser: style / layout / paint ───────────────────────────────────────
── later macrotask: Scheduler ─────────────────────────────────────────────
flushPassiveEffects → flushPassiveEffectsImpl → commitPassiveMountEffects → useEffect create()The steps
1. Module load: JSX becomes plain objects
Before React does anything, your bundler has turned <App /> into jsx(App, {}) (automatic runtime). In production jsx is jsxProd, which reuses the config object literal as props and calls ReactElement to build {$$typeof: REACT_ELEMENT_TYPE, type: App, key: null, ref: null, props: {}}.23 Nothing else happens. An element is an inert description, and none of App's code has run. useState is also just a forwarding stub at this point: it reads ReactSharedInternals.H, which is null outside a render.4
→ deep dive: Bootstrap
2. createRoot: FiberRoot, HostRoot, and every listener
createRoot(container) validates the container and calls createContainer, which calls createFiberRoot.56 That builds two objects pointing at each other:789
ReactDOMRoot { _internalRoot } ──► FiberRootNode
containerInfo: div#root
current ──────────────► HostRoot fiber (tag 3, ConcurrentMode)
pendingLanes: 0 stateNode ──► FiberRootNode
callbackNode: null memoizedState: {element: null, isDehydrated: false, cache}
updateQueue: {baseState, shared: {pending: null}, …}
alternate: null, child: nullinitializeUpdateQueue gives the HostRoot a class-style update queue.10 Then createRoot stamps the container with __reactContainer$<random> (markContainerAsRoot) and calls listenToAllSupportedEvents(container), which attaches one capture listener and (for bubbling events) one bubble listener per native event type.1112 All the event wiring the click in page 21 needs exists now, before any component has rendered.
→ deep dive: createRoot, Event System
3. root.render(<App/>) → updateContainer
ReactDOMRoot.prototype.render checks the root isn't unmounted and calls updateContainer(children, root, null, null).13 updateContainer reads container.current (the HostRoot fiber), picks a lane, and hands off to updateContainerImpl.14
→ deep dive: root.render
4. requestUpdateLane → DefaultLane
requestUpdateLane checks for a render-phase update (no), then for a transition (ReactSharedInternals.T is null), and finally returns eventPriorityToLane(resolveUpdatePriority()).15 The DOM's resolveUpdatePriority finds no priority set by a React event wrapper and, at top level in a module, window.event === undefined, so it returns DefaultEventPriority.16 Event priorities are lanes: DefaultEventPriority is defined as DefaultLane.17
SyncLane = 0b0000000000000000000000000000010
DefaultLane = 0b0000000000000000000000000100000 ← this update→ deep dive: Lanes & Scheduler
5. The update is created and buffered
updateContainerImpl creates Update {lane: DefaultLane, tag: UpdateState, payload: {element: <App/>}} and calls enqueueUpdate(rootFiber, update, lane).1920 That goes to enqueueConcurrentClassUpdate, which does not link the update into updateQueue.shared.pending yet.21 It pushes (fiber, queue, update, lane) onto the module-level concurrentQueues array and marks fiber.lanes |= DefaultLane right away.22 getRootForUpdatedFiber walks .return to the HostRoot, which for the root fiber is immediate, and returns the FiberRoot.23
concurrentQueues = [HostRoot, hostRootQueue, update{payload:{element}}, DefaultLane]
HostRoot.lanes = DefaultLane
HostRoot.updateQueue.shared.pending = null ← still empty; linked in step 10→ deep dive: root.render, setState (same buffer)
6. scheduleUpdateOnFiber marks the root
scheduleUpdateOnFiber(root, rootFiber, DefaultLane) calls markRootUpdated, which ORs the lane into root.pendingLanes and clears the suspended/pinged bookkeeping.2425 We're not inside a render, so it goes on to ensureRootIsScheduled(root). After this step, root.pendingLanes === DefaultLane is the only record that there is work to do.
7. ensureRootIsScheduled books one microtask
ensureRootIsScheduled appends the root to the scheduler's linked list of roots, sets mightHavePendingSyncWork, and calls ensureScheduleIsScheduled.2627 That queues at most one microtask per tick via scheduleImmediateRootScheduleTask, and in the DOM host config scheduleMicrotask is queueMicrotask.2829 root.render now returns. Your script carries on. If it called root.render again, the second update would join the same queue and the same microtask.
8. Microtask: pick lanes, book a Scheduler task
When the JS stack empties, processRootScheduleInMicrotask runs. For each scheduled root it calls scheduleTaskForRootDuringMicrotask.30 That calls getNextLanes, which returns DefaultLane. The result doesn't include SyncLane, so instead of flushing in the microtask, it maps the lane's event priority to NormalPriority and calls scheduleCallback(NormalPriority, performWorkOnRootViaSchedulerTask.bind(null, root)).31 The Scheduler pushes the task onto its min-heap and posts a MessageChannel message.3233
→ deep dive: Lanes & Scheduler
9. Scheduler task → performWorkOnRoot, and why it is not time-sliced
In a new macrotask, performWorkUntilDeadline → flushWork → workLoop calls the callback.343536 Between the microtask and this task the browser could paint, so any static HTML shell stays visible. performWorkOnRootViaSchedulerTask flushes leftover passive effects (none), recomputes getNextLanes (still DefaultLane), and calls performWorkOnRoot(root, lanes, forceSync).37
Now the important decision:38
const shouldTimeSlice =
(!forceSync && !includesBlockingLane(lanes) && !includesExpiredLane(root, lanes)) ||
checkIfRootIsPrerendering(root, lanes);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);includesBlockingLane tests against SyncLane | InputContinuousLane | DefaultLane (plus their hydration variants and GestureLane).39 DefaultLane is in that set, so shouldTimeSlice is false and the first render runs through renderRootSync, in one go, never checking shouldYield(). Only transition, retry, idle and offscreen lanes reach renderRootConcurrent.
→ deep dive: Work Loop
10. prepareFreshStack: a WIP HostRoot, and the update gets linked
renderRootSync sets executionContext |= RenderContext, pushes the dispatcher, and sees workInProgressRoot !== root, so it calls prepareFreshStack(root, DefaultLane).4041 Two things matter there:
createWorkInProgress(root.current, null). The HostRoot has no alternate, so a second HostRoot fiber is allocated and the two are cross-linked (wip.alternate = current,current.alternate = wip). The WIP copiesupdateQueue,memoizedState,lanesandchild(null).42finishQueueingConcurrentUpdates()drainsconcurrentQueues. The update is spliced intoqueue.shared.pending(a one-element circular list), andmarkUpdateLaneFromFiberToRootwould setchildLaneson ancestors (the HostRoot has none).43
FiberRoot.current ──► HostRoot(A) ◄──alternate──► HostRoot(B) = workInProgress
shared.pending ──► update ⟲ (both share the same queue.shared)11. beginWork(HostRoot): the queue becomes the element
workLoopSync calls performUnitOfWork until workInProgress === null.4445 The first unit is HostRoot(B). beginWork sees current !== null, with identical (null) props, but checkScheduledUpdateOrContext finds DefaultLane in current.lanes, so it doesn't bail. It sets workInProgress.lanes = NoLanes and dispatches to updateHostRoot.46 updateHostRoot clones the queue and runs processUpdateQueue, which applies the payload: memoizedState = {element: <App/>, …}.4748 nextChildren is that element, so it calls reconcileChildren.
→ deep dive: beginWork & Reconciliation
12. Placement only on the top
The HostRoot has a current, so reconcileChildren uses reconcileChildFibers, the reconciler with side-effect tracking on.49 reconcileSingleElement finds no existing child, calls createFiberFromElement (a function without prototype.isReactComponent becomes a FunctionComponent fiber), and placeSingleChild sees alternate === null and sets Placement on the App fiber.50515253
Every fiber below App has current === null and goes through mountChildFibers, which is the same reconciler with tracking off. None of them get Placement. That's the setup for the single DOM insertion in step 18.
13. beginWork(App): renderWithHooks with the mount dispatcher
updateFunctionComponent calls renderWithHooks.54 renderWithHooks sets currentlyRenderingFiber = App, nulls memoizedState and updateQueue, and because current === null installs HooksDispatcherOnMount in ReactSharedInternals.H. Then it calls App(props).5556 Your code runs for the first time.
14. Hooks build the list
Each hook call appends a node via mountWorkInProgressHook.57
useState(0)→mountState→mountStateImplcreates the hook and its queue{pending: null, lanes: NoLanes, lastRenderedReducer: basicStateReducer, lastRenderedState: 0}, and bindsdispatch = dispatchSetState.bind(null, App fiber, queue).5859 That bound function is thesetCountyouronClickcloses over.useLayoutEffect→mountLayoutEffectsets fiber flagsUpdate | LayoutStaticand pushes an effect taggedHookHasEffect | HookLayout.6061useEffect→mountEffectsetsPassive | PassiveStaticand pushes an effect taggedHookHasEffect | HookPassive.6263
App fiber
flags: Placement | Update | Passive (+ static flags)
memoizedState ─► Hook{useState: 0, queue}
└ next ─► Hook{Effect{Layout|HasEffect, deps:[0]}}
└ next ─► Hook{Effect{Passive|HasEffect, deps:[0]}}
updateQueue: {lastEffect ─► passive effect ⟲ layout effect, events: null, stores: null, memoCache: null}App returns a <div> element. renderWithHooks swaps in ContextOnlyDispatcher (calling a hook now would throw) and returns it. reconcileChildren (mount variant) creates the div fiber.
→ deep dive: Hooks on Mount
15. Children all the way down
beginWork(div) → updateHostComponent pushes host context, and props.children is an array, so reconcileChildrenArray creates three fibers: Header, button and Footer.6465 Then the depth-first walk continues:
performUnitOfWork copies pendingProps into memoizedProps after each beginWork. That stored props object is what the next render compares by identity.45 completeUnitOfWork drives the climb.66
→ deep dive: Work Loop, beginWork & Reconciliation
16. completeWork: DOM built off-screen
For each HostComponent with no current, completeWork calls createInstance (document.createElement, plus precacheFiberNode and updateFiberProps, which write the __reactFiber$ and __reactProps$ expandos the event system reads later).67686970 It then calls appendAllChildren, which appends the nearest host descendants through component fibers. At div it skips over Header and Footer and appends h1, button and footer.7172 Finally finalizeInitialChildren → setInitialProperties sets attributes, and setProp('children', 0) sets the button's textContent to "0".737475 onClick isn't an attribute. setProp only installs Safari's no-op onclick workaround (trapClickOnNonInteractiveElement).76
detached: <div><h1>Counter</h1><button>0</button><footer>made with React</footer></div>
document: <div id="root"></div> ← untouched so farAt the HostRoot, completeWork sees a first client render (current.child === null, not hydrating) and sets Snapshot, which schedules clearContainer for the commit.77
→ deep dive: completeWork
17. bubbleProperties: summaries for the commit
Every completeWork ends with bubbleProperties, which ORs children's flags | subtreeFlags into the parent's subtreeFlags and merges lanes | childLanes into childLanes.78 At the end:
HostRoot(B) flags: Snapshot subtreeFlags: Placement|Update|Passive|… childLanes: 0
App flags: Placement|Update|Passive
div, h1, button, footer, Header, Footer: no mutation flagsThe commit can now ask "is there anything of kind X below here?" with one bitmask test per fiber.
18. Commit: completeRoot → commitRoot, mutation, the single insertion
performWorkOnRoot reads finishedWork = root.current.alternate and calls finishConcurrentRender (used for sync renders too). That calls completeRootWhenReady → completeRoot → commitRoot.7980818283 There is no commitRootImpl in 19.3.1. commitRoot:
markRootFinishedclearsDefaultLanefrompendingLanes.- The tree has
Passivein itssubtreeFlags, so it postsscheduleCallback(NormalSchedulerPriority, flushPassiveEffects)before any DOM work.84 - Before mutation:
commitBeforeMutationEffectsdisables React's event system, and for the HostRoot'sSnapshotcallsclearContainer, removing any placeholder markup.858687 - Mutation:
flushMutationEffects→commitMutationEffects→commitMutationEffectsOnFiberrecurses wheresubtreeFlags & MutationMask, andcommitReconciliationEffectsfindsPlacementon App.88899091commitHostPlacement→commitPlacementfinds the host parent (the HostRoot, socontainerInfo). App isn't a host node, soinsertOrAppendPlacementNodeIntoContainerrecurses todivand callsappendChildToContainer(container, div).92939495 That one call puts the whole UI into the document.
→ deep dive: Commit Phase
19. The swap: root.current = finishedWork
At the end of flushMutationEffects, React does root.current = finishedWork (line 4051).96 It sits after mutation, so unmount-style code would still see the old tree, and before layout, so useLayoutEffect and refs see the new one. HostRoot(B) is now current. HostRoot(A) is the scratch buffer for the next render.
20. Layout effects (before paint)
flushLayoutEffects → commitLayoutEffects → commitLayoutEffectOnFiber, children first, pruned by LayoutMask.979899 App has Update, so commitHookLayoutEffects → commitHookEffectListMount(HookLayout | HookHasEffect) runs the layout create function: document.title = "count: 0".100101 The DOM is live, so measuring here returns real geometry (with a forced synchronous layout). Nothing has painted.
21. flushSpawnedWork → requestPaint → pixels
flushSpawnedWork calls requestPaint(), which sets Scheduler's needsPaint.102103 The lanes were DefaultLane, not SyncLane, so the "flush passive effects synchronously" branch is skipped.104 The task returns. Back in Scheduler's workLoop, shouldYieldToHost() now returns true because needsPaint is set, so Scheduler stops and posts another message instead of running the passive-effects task straight away.105 The JS task ends, and the browser runs style, layout and paint. The user sees "Counter / 0 / made with React".
22. Passive effects in the next task
The next Scheduler macrotask runs the callback posted in step 18: flushPassiveEffects → flushPassiveEffectsImpl. That first runs all passive unmounts (none), then commitPassiveMountEffects → commitPassiveMountOnFiber → commitHookPassiveMountEffects → your useEffect create function. console.log("committed", 0).106107108109 A setState here would get at most DefaultLane and be batched normally.
With the React Compiler
This is the real babel-plugin-react-compiler@1.0.0 output for the example (file compile-demo/examples/Walkthrough.jsx → out-Walkthrough.js in the scratchpad, verbatim apart from the comments I added):
import { c as _c } from "react/compiler-runtime";
function Header(t0) {
const $ = _c(2);
const { title } = t0;
let t1;
if ($[0] !== title) {
t1 = <h1>{title}</h1>;
$[0] = title;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
function Footer() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <footer>made with React</footer>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function App() {
const $ = _c(13);
const [count, setCount] = useState(0);
let t0;
let t1;
if ($[0] !== count) {
t0 = () => { document.title = `count: ${count}`; };
t1 = [count];
$[0] = count; $[1] = t0; $[2] = t1;
} else {
t0 = $[1]; t1 = $[2];
}
useLayoutEffect(t0, t1);
let t2;
let t3;
if ($[3] !== count) {
t2 = () => { console.log("committed", count); };
t3 = [count];
$[3] = count; $[4] = t2; $[5] = t3;
} else {
t2 = $[4]; t3 = $[5];
}
useEffect(t2, t3);
let t4;
let t5;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Header title="Counter" />; // no reactive inputs: built once per fiber
t5 = () => setCount(_temp); // setCount is stable, so the handler is too
$[6] = t4; $[7] = t5;
} else {
t4 = $[6]; t5 = $[7];
}
let t6;
if ($[8] !== count) {
t6 = <button onClick={t5}>{count}</button>;
$[8] = count; $[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Footer />;
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] !== t6) {
t8 = <div>{t4}{t6}{t7}</div>;
$[11] = t6; $[12] = t8;
} else {
t8 = $[12];
}
return t8;
}
function _temp(c) {
return c + 1; // the updater, outlined to module scope
}(I joined some short statements onto one line.) Now re-walk the 22 steps:
-
Steps 1–12: unchanged. The compiler doesn't touch
react-dom, the root, lanes, the scheduler orbeginWork(HostRoot).createRoot(...).render(<App />)at module level isn't inside a component, so it is left alone. The_cimport resolves touseMemoCache:react/compiler-runtimeis literallyexport {useMemoCache as c} from './ReactHooks'.110 -
Step 13: one extra hook call first.
App()begins with_c(13), dispatched through the mount dispatcher touseMemoCache.56111 There is no cache on the fiber or its (nonexistent) alternate, so it allocates{data: [], index: 0}, creates theFunctionComponentUpdateQueue, stores the cache infiber.updateQueue.memoCache, and returns a 13-slot array filled withREACT_MEMO_CACHE_SENTINEL(Symbol.for('react.memo_cache_sentinel')).111112 It doesn't add a node to the hook list. -
Step 14: same hook list, plus cache filling. Every
$[i] !== deptest is true against the sentinel, and every=== sentineltest is true by definition, so every scope runs once and writes its slots.useState,useLayoutEffectanduseEffectproduce exactly the hook nodes and effect ring shown in step 14. The effects are pushed into the sameupdateQueueobject that holds the cache.TextApp.updateQueue.memoCache.data[0] = [0, layoutFn, [0], 0, passiveFn, [0], <Header/>, onClickFn, 0, <button/>, <Footer/>, <button/>, <div/>] 0 1 2 3 4 5 6 7 8 9 10 11 12 -
Step 15: same walk.
HeaderandFooterare compiled too.Headercaches<h1>ontitle(_c(2)), andFootercaches<footer>behind a sentinel (_c(1)). Each fills its own cache on its own fiber. Every component still runs exactly once, and every fiber is created exactly as before:current === nulleverywhere, so no bailout check applies. -
Steps 16–22: unchanged. Same DOM nodes, same single
appendChildToContainer, same swap, same layout effect, same paint, same passive effect in the next task. The cached element objects simply become thependingPropssource for the fibers, as uncompiled elements would.
The cost on mount is three small arrays (13 + 2 + 1 slots), a few comparisons and stores, and the closures that would have been created anyway. The payoff shows up only on the next render, which is the subject of setState Walkthrough: the cached <Header/> and <Footer/> elements are what let React skip those subtrees.
Step by step comparison
Summary
createRoot(el).render(<App/>) renders nothing synchronously: it builds the root objects, attaches the listeners, buffers one DefaultLane update and books a microtask. The microtask books a Scheduler task, which renders the whole tree with renderRootSync, because DefaultLane is blocking and never time-sliced. The DOM is built off-screen, inserted with one appendChildToContainer, layout effects run, the browser paints, and useEffect runs in a later task. The compiler only adds cache filling; its payoff comes on the next render, in the click walkthrough.
Key takeaways
- JSX and
createRoot: elements are inert objects;createRootbuilds theFiberRootNodeand HostRoot fiber and callslistenToAllSupportedEventsbefore any component runs. requestUpdateLane: with no React event and no transition, a top-levelroot.renderresolves toDefaultEventPriority, which isDefaultLane.- Buffered update:
enqueueConcurrentClassUpdateparks the update inconcurrentQueues;prepareFreshStacklinks it intoshared.pendingonly when the render starts (root.render). - One microtask, then a task:
ensureRootIsScheduledqueues one microtask, andscheduleTaskForRootDuringMicrotaskbooks aNormalPriorityScheduler task because the lanes don't includeSyncLane(Lanes & Scheduler). - Not time-sliced:
includesBlockingLanecoversDefaultLane, soshouldTimeSliceisfalse;createRootmakes the first render asynchronous, not interruptible (Work Loop). Placementonly on App: the HostRoot reconciles with side-effect tracking on, while everything below goes throughmountChildFibers.- Hooks on mount:
HooksDispatcherOnMountbuilds the hook list, bindssetCounttodispatchSetState, and flags the fiberUpdate | Passive. completeWork:createInstancewrites the__reactFiber$/__reactProps$expandos,appendAllChildrenassembles the detached tree, andbubblePropertiessummarises flags (completeWork).- Commit: passive effects are scheduled first,
clearContainerruns, oneappendChildToContainerinserts the UI, thenroot.current = finishedWorkand layout effects run before paint (Commit Phase). - Paint, then
useEffect:requestPaintmakes Scheduler yield, the browser paints, and the passive effect runs in the next task because this commit was notSyncLane.
Footnotes
-
shouldSetTextContent— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L744-L755 (opens in a new tab) ↩ -
jsxProd— packages/react/src/jsx/ReactJSXElement.js#L291-L348 (opens in a new tab) ↩ -
ReactElement— packages/react/src/jsx/ReactJSXElement.js#L170-L283 (opens in a new tab) ↩ -
useState/resolveDispatcherin thereactpackage — packages/react/src/ReactHooks.js#L24-L71 (opens in a new tab) ↩ -
createRoot— packages/react-dom/src/client/ReactDOMRoot.js#L171-L262 (opens in a new tab) ↩ -
createContainer— packages/react-reconciler/src/ReactFiberReconciler.js#L235-L280 (opens in a new tab) ↩ -
createFiberRoot— packages/react-reconciler/src/ReactFiberRoot.js#L157-L236 (opens in a new tab) ↩ -
FiberRootNode— packages/react-reconciler/src/ReactFiberRoot.js#L50-L155 (opens in a new tab) ↩ -
createHostRootFiber— packages/react-reconciler/src/ReactFiber.js#L529-L551 (opens in a new tab) ↩ -
initializeUpdateQueue— packages/react-reconciler/src/ReactFiberClassUpdateQueue.js#L176-L189 (opens in a new tab) ↩ -
markContainerAsRoot— packages/react-dom-bindings/src/client/ReactDOMComponentTree.js#L113-L116 (opens in a new tab) ↩ -
listenToAllSupportedEvents— packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L432-L459 (opens in a new tab) ↩ -
ReactDOMRoot.prototype.render— packages/react-dom/src/client/ReactDOMRoot.js#L107-L136 (opens in a new tab) ↩ -
updateContainer— packages/react-reconciler/src/ReactFiberReconciler.js#L353-L370 (opens in a new tab) ↩ -
requestUpdateLane— packages/react-reconciler/src/ReactFiberWorkLoop.js#L812-L868 (opens in a new tab) ↩ -
resolveUpdatePriority(DOM) — packages/react-dom-bindings/src/client/ReactDOMUpdatePriority.js#L35-L45 (opens in a new tab) ↩ -
Event priorities defined as lanes — packages/react-reconciler/src/ReactEventPriorities.js#L24-L28 (opens in a new tab) ↩
-
SyncLane,InputContinuousLane,DefaultLane— packages/react-reconciler/src/ReactFiberLane.js#L47-L54 (opens in a new tab) ↩ -
updateContainerImpl— packages/react-reconciler/src/ReactFiberReconciler.js#L393-L458 (opens in a new tab) ↩ -
createUpdate— packages/react-reconciler/src/ReactFiberClassUpdateQueue.js#L210-L221 (opens in a new tab) ↩ -
enqueueUpdate(class queue) — packages/react-reconciler/src/ReactFiberClassUpdateQueue.js#L223-L274 (opens in a new tab) ↩ -
enqueueUpdate(concurrent buffer) — packages/react-reconciler/src/ReactFiberConcurrentUpdates.js#L90-L113 (opens in a new tab) ↩ -
getRootForUpdatedFiber— packages/react-reconciler/src/ReactFiberConcurrentUpdates.js#L252-L276 (opens in a new tab) ↩ -
scheduleUpdateOnFiber— packages/react-reconciler/src/ReactFiberWorkLoop.js#L987-L1113 (opens in a new tab) ↩ -
markRootUpdated— packages/react-reconciler/src/ReactFiberLane.js#L825-L849 (opens in a new tab) ↩ -
ensureRootIsScheduled— packages/react-reconciler/src/ReactFiberRootScheduler.js#L116-L152 (opens in a new tab) ↩ -
ensureScheduleIsScheduled— packages/react-reconciler/src/ReactFiberRootScheduler.js#L154-L169 (opens in a new tab) ↩ -
scheduleImmediateRootScheduleTask— packages/react-reconciler/src/ReactFiberRootScheduler.js#L650-L696 (opens in a new tab) ↩ -
scheduleMicrotask=queueMicrotask— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L871-L877 (opens in a new tab) ↩ -
processRootScheduleInMicrotask— packages/react-reconciler/src/ReactFiberRootScheduler.js#L259-L348 (opens in a new tab) ↩ -
scheduleTaskForRootDuringMicrotask— packages/react-reconciler/src/ReactFiberRootScheduler.js#L384-L509 (opens in a new tab) ↩ -
unstable_scheduleCallback— packages/scheduler/src/forks/Scheduler.js#L335-L427 (opens in a new tab) ↩ -
MessageChannelhost callback — packages/scheduler/src/forks/Scheduler.js#L546-L554 (opens in a new tab) ↩ -
performWorkUntilDeadline— packages/scheduler/src/forks/Scheduler.js#L498-L528 (opens in a new tab) ↩ -
flushWork— packages/scheduler/src/forks/Scheduler.js#L145-L191 (opens in a new tab) ↩ -
Scheduler
workLoop— packages/scheduler/src/forks/Scheduler.js#L193-L266 (opens in a new tab) ↩ -
performWorkOnRootViaSchedulerTask— packages/react-reconciler/src/ReactFiberRootScheduler.js#L513-L606 (opens in a new tab) ↩ -
shouldTimeSliceinperformWorkOnRoot— packages/react-reconciler/src/ReactFiberWorkLoop.js#L1165-L1181 (opens in a new tab) ↩ -
includesBlockingLane— packages/react-reconciler/src/ReactFiberLane.js#L684-L694 (opens in a new tab) ↩ -
renderRootSync— packages/react-reconciler/src/ReactFiberWorkLoop.js#L2623-L2768 (opens in a new tab) ↩ -
prepareFreshStack(createWorkInProgressat L2256,finishQueueingConcurrentUpdatesat L2284) — packages/react-reconciler/src/ReactFiberWorkLoop.js#L2023-L2293 (opens in a new tab) ↩ -
createWorkInProgress— packages/react-reconciler/src/ReactFiber.js#L323-L437 (opens in a new tab) ↩ -
finishQueueingConcurrentUpdates— packages/react-reconciler/src/ReactFiberConcurrentUpdates.js#L50-L84 (opens in a new tab) ↩ -
workLoopSync— packages/react-reconciler/src/ReactFiberWorkLoop.js#L2772-L2777 (opens in a new tab) ↩ -
performUnitOfWork— packages/react-reconciler/src/ReactFiberWorkLoop.js#L3081-L3123 (opens in a new tab) ↩ ↩2 -
beginWork(bailout gate L4250-L4320, tag switch after) — packages/react-reconciler/src/ReactFiberBeginWork.js#L4226-L4512 (opens in a new tab) ↩ -
updateHostRoot— packages/react-reconciler/src/ReactFiberBeginWork.js#L1831-L1949 (opens in a new tab) ↩ -
processUpdateQueue— packages/react-reconciler/src/ReactFiberClassUpdateQueue.js#L487-L698 (opens in a new tab) ↩ -
reconcileChildren— packages/react-reconciler/src/ReactFiberBeginWork.js#L339-L370 (opens in a new tab) ↩ -
reconcileSingleElement— packages/react-reconciler/src/ReactChildFiber.js#L1703-L1806 (opens in a new tab) ↩ -
createFiberFromElement— packages/react-reconciler/src/ReactFiber.js#L739-L765 (opens in a new tab) ↩ -
createFiberFromTypeAndProps(defaults toFunctionComponent) — packages/react-reconciler/src/ReactFiber.js#L554-L737 (opens in a new tab) ↩ -
placeSingleChild— packages/react-reconciler/src/ReactChildFiber.js#L543-L550 (opens in a new tab) ↩ -
updateFunctionComponent— packages/react-reconciler/src/ReactFiberBeginWork.js#L1453-L1562 (opens in a new tab) ↩ -
renderWithHooks(dispatcher choice at L562-L566) — packages/react-reconciler/src/ReactFiberHooks.js#L505-L634 (opens in a new tab) ↩ -
HooksDispatcherOnMount— packages/react-reconciler/src/ReactFiberHooks.js#L3926-L3952 (opens in a new tab) ↩ ↩2 -
mountWorkInProgressHook— packages/react-reconciler/src/ReactFiberHooks.js#L983-L1002 (opens in a new tab) ↩ -
mountState— packages/react-reconciler/src/ReactFiberHooks.js#L1948-L1960 (opens in a new tab) ↩ -
mountStateImpl— packages/react-reconciler/src/ReactFiberHooks.js#L1920-L1946 (opens in a new tab) ↩ -
mountLayoutEffect— packages/react-reconciler/src/ReactFiberHooks.js#L2796-L2808 (opens in a new tab) ↩ -
mountEffectImpl— packages/react-reconciler/src/ReactFiberHooks.js#L2640-L2655 (opens in a new tab) ↩ -
mountEffect— packages/react-reconciler/src/ReactFiberHooks.js#L2697-L2719 (opens in a new tab) ↩ -
pushSimpleEffect— packages/react-reconciler/src/ReactFiberHooks.js#L2588-L2603 (opens in a new tab) ↩ -
updateHostComponent(begin phase) — packages/react-reconciler/src/ReactFiberBeginWork.js#L1966-L2033 (opens in a new tab) ↩ -
reconcileChildrenArray— packages/react-reconciler/src/ReactChildFiber.js#L1177-L1369 (opens in a new tab) ↩ -
completeUnitOfWork— packages/react-reconciler/src/ReactFiberWorkLoop.js#L3368-L3431 (opens in a new tab) ↩ -
completeWork,HostComponentcase — packages/react-reconciler/src/ReactFiberCompleteWork.js#L1374-L1472 (opens in a new tab) ↩ -
createInstance— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L534-L664 (opens in a new tab) ↩ -
precacheFiberNode— packages/react-dom-bindings/src/client/ReactDOMComponentTree.js#L94-L111 (opens in a new tab) ↩ -
updateFiberProps— packages/react-dom-bindings/src/client/ReactDOMComponentTree.js#L295-L301 (opens in a new tab) ↩ -
appendAllChildren— packages/react-reconciler/src/ReactFiberCompleteWork.js#L245-L352 (opens in a new tab) ↩ -
appendInitialChild— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L695-L701 (opens in a new tab) ↩ -
finalizeInitialChildren— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L703-L721 (opens in a new tab) ↩ -
setInitialProperties— packages/react-dom-bindings/src/client/ReactDOMComponent.js#L1111-L1496 (opens in a new tab) ↩ -
setProp,case 'children'(string/number →setTextContent) — packages/react-dom-bindings/src/client/ReactDOMComponent.js#L406-L434 (opens in a new tab) ↩ -
setProp,case 'onClick'→trapClickOnNonInteractiveElement— packages/react-dom-bindings/src/client/ReactDOMComponent.js#L618-L627 (opens in a new tab) ↩ -
completeWork,HostRootcase (Snapshotat L1178) — packages/react-reconciler/src/ReactFiberCompleteWork.js#L1119-L1198 (opens in a new tab) ↩ -
bubbleProperties— packages/react-reconciler/src/ReactFiberCompleteWork.js#L793-L914 (opens in a new tab) ↩ -
performWorkOnRoot(finishedWork = root.current.alternateat L1222) — packages/react-reconciler/src/ReactFiberWorkLoop.js#L1137-L1325 (opens in a new tab) ↩ -
finishConcurrentRender— packages/react-reconciler/src/ReactFiberWorkLoop.js#L1420-L1576 (opens in a new tab) ↩ -
completeRootWhenReady— packages/react-reconciler/src/ReactFiberWorkLoop.js#L1578-L1701 (opens in a new tab) ↩ -
completeRoot— packages/react-reconciler/src/ReactFiberWorkLoop.js#L3511-L3723 (opens in a new tab) ↩ -
commitRoot— packages/react-reconciler/src/ReactFiberWorkLoop.js#L3725-L3922 (opens in a new tab) ↩ -
commitRootposting the passive-effects callback — packages/react-reconciler/src/ReactFiberWorkLoop.js#L3771-L3820 (opens in a new tab) ↩ -
commitBeforeMutationEffects— packages/react-reconciler/src/ReactFiberCommitWork.js#L352-L371 (opens in a new tab) ↩ -
commitBeforeMutationEffectsOnFiber— packages/react-reconciler/src/ReactFiberCommitWork.js#L483-L579 (opens in a new tab) ↩ -
clearContainer— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3972-L3988 (opens in a new tab) ↩ -
flushMutationEffects— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4009-L4053 (opens in a new tab) ↩ -
commitMutationEffects— packages/react-reconciler/src/ReactFiberCommitWork.js#L2027-L2044 (opens in a new tab) ↩ -
commitMutationEffectsOnFiber— packages/react-reconciler/src/ReactFiberCommitWork.js#L2072-L2843 (opens in a new tab) ↩ -
commitReconciliationEffects— packages/react-reconciler/src/ReactFiberCommitWork.js#L2845-L2864 (opens in a new tab) ↩ -
commitHostPlacement— packages/react-reconciler/src/ReactFiberCommitHostEffects.js#L585-L595 (opens in a new tab) ↩ -
commitPlacement— packages/react-reconciler/src/ReactFiberCommitHostEffects.js#L452-L543 (opens in a new tab) ↩ -
insertOrAppendPlacementNodeIntoContainer— packages/react-reconciler/src/ReactFiberCommitHostEffects.js#L330-L392 (opens in a new tab) ↩ -
appendChildToContainer— packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L1082-L1133 (opens in a new tab) ↩ -
root.current = finishedWork— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4047-L4051 (opens in a new tab) ↩ -
flushLayoutEffects— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4055-L4152 (opens in a new tab) ↩ -
commitLayoutEffects— packages/react-reconciler/src/ReactFiberCommitWork.js#L3063-L3078 (opens in a new tab) ↩ -
commitLayoutEffectOnFiber— packages/react-reconciler/src/ReactFiberCommitWork.js#L600-L934 (opens in a new tab) ↩ -
commitHookLayoutEffects— packages/react-reconciler/src/ReactFiberCommitEffects.js#L97-L112 (opens in a new tab) ↩ -
commitHookEffectListMount— packages/react-reconciler/src/ReactFiberCommitEffects.js#L141-L247 (opens in a new tab) ↩ -
flushSpawnedWork(requestPaint()at L4189) — packages/react-reconciler/src/ReactFiberWorkLoop.js#L4154-L4437 (opens in a new tab) ↩ -
Scheduler
requestPaint— packages/scheduler/src/forks/Scheduler.js#L474-L479 (opens in a new tab) ↩ -
Sync-lane passive flush in
flushSpawnedWork— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4316-L4329 (opens in a new tab) ↩ -
shouldYieldToHost— packages/scheduler/src/forks/Scheduler.js#L459-L472 (opens in a new tab) ↩ -
flushPassiveEffects— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4691-L4726 (opens in a new tab) ↩ -
flushPassiveEffectsImpl— packages/react-reconciler/src/ReactFiberWorkLoop.js#L4728-L4875 (opens in a new tab) ↩ -
commitPassiveMountEffects— packages/react-reconciler/src/ReactFiberCommitWork.js#L3764-L3780 (opens in a new tab) ↩ -
commitHookPassiveMountEffects— packages/react-reconciler/src/ReactFiberCommitEffects.js#L305-L316 (opens in a new tab) ↩ -
react/compiler-runtimere-export — packages/react/src/ReactCompilerRuntime.js#L10 (opens in a new tab) ↩ -
useMemoCache— packages/react-reconciler/src/ReactFiberHooks.js#L1181-L1263 (opens in a new tab) ↩ ↩2 -
REACT_MEMO_CACHE_SENTINEL— packages/shared/ReactSymbols.js#L41-L43 (opens in a new tab) ↩