import { FiberTag, createFiber, REACT_ELEMENT_TYPE, REACT_LEGACY_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, type Fiber, type FiberRoot, type ReactElement, type ReactNode, type Hook, type Effect, } from '../core' import { ReactSharedInternals, REACT_LAZY_TYPE, REACT_STRICT_MODE_TYPE, REACT_PROFILER_TYPE, } from '../react' import { createHostNode, setProp } from './dom' import { makeDispatcher } from './dispatcher' import { adoptHostDom, adoptTextDom, setHydrationCursor, getHydrationCursor, clearHydrationCursor, findHostParent as findHydrationHost, abortHydration, } from './features/hydration' // --------------------------------------------------------------------------- // Render scheduling // --------------------------------------------------------------------------- let currentRoot: FiberRoot | null = null let flushing = false let isBatching = false const pendingRoots = new Set() // Set by rerenderFiber to identify the exact memo-tagged fiber whose INTERNAL // state (hook update, useSyncExternalStore notification) triggered this render // pass. renderMemo checks this to bypass its prop-equality gate for that fiber. // Without the bypass, a memo bail would swallow state changes: React's memo is // only a parent-triggered gate — state-driven rerenders must always run the // inner function. Router-adjacent components (Outlet, Match, MatchInner) are // all memo-wrapped and subscribe to stores; missing this bypass breaks nav // content updates even though the URL changes. let forceRerenderingFiber: Fiber | null = null export function scheduleUpdate(fiber: Fiber): void { // Drop updates scheduled on already-um fibers. Subscribers (router, // query, any external store) can fire after unmount if their cleanup was // missed, and letting those reach rerenderFiber mounts zombie DOM into the // old .parent's DOM (which stays reachable via the stale pointer). if (fiber.um) return const root = findRoot(fiber) if (!root) return root.p.add(fiber) fiber.dy = true pendingRoots.add(root) if (isBatching) return if (!root.s) { root.s = true queueMicrotask(flushPending) } } export function flushSyncWork(fn: () => void): void { const wasBatching = isBatching isBatching = true try { fn() } finally { isBatching = wasBatching } flushPending() } export function batchedUpdates(fn: () => T): T { const wasBatching = isBatching isBatching = true try { return fn() } finally { isBatching = wasBatching if (!wasBatching) flushPending() } } function flushPending(): void { if (flushing) return flushing = true try { let guard = 0 while (pendingRoots.size > 0) { if (++guard > 50) { if (process.env.NODE_ENV !== 'production') { throw new Error('flushPending exceeded 50 iterations — suspected infinite update loop.') } throw new Error() } const roots = [...pendingRoots] pendingRoots.clear() for (const root of roots) { root.s = false // Render each pending fiber from shallowest first so an ancestor's // cascade reaches descendants before we try to render them directly. // Descendants rendered via cascade still have `dy=true` (only // rerenderFiber clears it); when we later reach them in this loop, // rerenderFiber's own `if (!dy) return` is our short-circuit. We // previously filtered descendants of dy ancestors here, but that // loses updates whenever an ancestor's render doesn't actually reach // the descendant — e.g. React.memo bailing on equal props. Keep all // dy fibers and let rerenderFiber de-dupe via its dy check. const pending = [...root.p] root.p.clear() pending.sort((a, b) => fiberDepth(a) - fiberDepth(b)) for (const fiber of pending) { rerenderFiber(fiber, root) } runEffects(root) } } } finally { flushing = false } } export function discardPendingWork(root: FiberRoot): void { root.p.clear() root.s = false pendingRoots.delete(root) } function fiberDepth(fiber: Fiber): number { let d = 0 let p: Fiber | null = fiber.parent while (p) { d++ p = p.parent } return d } export function findRoot(fiber: Fiber): FiberRoot | null { let f: Fiber | null = fiber while (f) { if (f.root) return f.root f = f.parent } return null } // --------------------------------------------------------------------------- // Entry points (called by createRoot) // --------------------------------------------------------------------------- export function renderRoot(root: FiberRoot, children: ReactNode): void { const rootFiber = root.r rootFiber.pp = { children } currentRoot = root try { reconcileChildren(rootFiber, childrenToArray(children), root.c as Node, null) rootFiber.mp = rootFiber.pp rootFiber.dy = false } finally { currentRoot = null } runEffects(root) } function rerenderFiber(fiber: Fiber, root: FiberRoot): void { if (!fiber.dy) return // Skip fibers that were um between scheduling and flush. Without this, // the flush loop re-enters a zombie fiber whose .parent is still set; its // render mounts fresh DOM into the old parent's still-attached DOM (since // unmountFiber only clears fiber.child, not fiber.parent). Visible as route // content from a previous location staying on screen after nav, because a // pending rerender on the old route's LibraryLandingPage (um during // Outlet's shallow-first render) still fires from root.pending. if (fiber.um) return // Clear BEFORE rendering so a scheduleUpdate() triggered mid-render (e.g. // error boundary catching a descendant throw) marks us dy for the next // flush iteration instead of being wiped out when render() completes. fiber.dy = false currentRoot = root // If this rerender is resuming a hydration that was deferred by a suspension, // re-activate hydration mode for its duration so descendants adopt DOM // instead of re-creating it. const resumeHydration = fiber.ms && (fiber.ms as any).p === true const prevHydrating = root.h if (resumeHydration) { delete (fiber.ms as any).p root.h = true } const prevForcing = forceRerenderingFiber forceRerenderingFiber = fiber try { renderFiber(fiber, getHostParent(fiber), getAnchor(fiber)) } finally { forceRerenderingFiber = prevForcing if (resumeHydration) { root.h = prevHydrating // Deferred hydration completed — detach the preserved cursor so future // updates (post-hydration state changes) don't try to adopt stale DOM. clearHydrationCursor(fiber) } currentRoot = null } } // --------------------------------------------------------------------------- // Element → children normalization // --------------------------------------------------------------------------- // Text children pass through as raw strings — no wrapper. The previous // `{_text: string}` shape allocated tens of thousands of objects per // stable-list re-render and dominated minor-GC pressure. `typeof === 'string'` // is also robust to RSC renderable proxies (which have `has` traps that // would fool a `'_text' in child` predicate but can't fool `typeof`). type NormalizedChild = ReactElement | string | null function isTextChild(child: Exclude): child is string { return typeof child === 'string' } export function childrenToArray(children: ReactNode): NormalizedChild[] { const out: NormalizedChild[] = [] pushChildren(children, out) return out } function pushChildren(node: ReactNode, out: NormalizedChild[]): void { if (node == null || typeof node === 'boolean') return if (typeof node === 'string') { // Empty strings render no text node (matches React + the `` // separator elision on the SSR side so server/client agree). if (node === '') return out.push(node) return } if (typeof node === 'number') { out.push('' + node) return } if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) pushChildren(node[i], out) return } if (isIterable(node)) { for (const item of node as Iterable) pushChildren(item, out) return } if (typeof node === 'object') { const t = (node as any).$$typeof if (ACCEPTED_ELEMENT_MARKERS.has(t)) { out.push(node as ReactElement) return } // Raw React.lazy as a child. RSC Flight encodes 'use client' components // (CodeBlock, CodeExplorer, etc.) as bare Lazy objects in the tree, not // wrapped in REACT_ELEMENT_TYPE. Dropping them made code snippets // disappear from docs pages. The RSC decoder pre-awaits payloads via // `awaitLazyElements`, so by render time the status is 'fulfilled' and // `_init()` returns the resolved element synchronously. if (t === REACT_LAZY_TYPE) { const lazy = node as any const resolved = lazy._init(lazy._payload) pushChildren(resolved, out) return } } } function isIterable(obj: any): boolean { return obj != null && typeof obj[Symbol.iterator] == 'function' } function getKeyOf(child: NormalizedChild, index: number): string { if (!child) return 'n' + index if (isTextChild(child)) return '$t' + index if (child.key != null) return 'k' + child.key return 'i' + index } function sameType(fiber: Fiber, child: NormalizedChild): boolean { if (!child) return false if (isTextChild(child)) return fiber.tag === FiberTag.Text return fiber.type === child.type && sameKey(fiber.key, child.key) } function sameKey(a: string | null, b: string | null | undefined): boolean { return (a ?? null) === (b ?? null) } // --------------------------------------------------------------------------- // Fiber creation // --------------------------------------------------------------------------- function fiberFromChild(child: NormalizedChild, parent: Fiber): Fiber { if (!child) return createFiber(FiberTag.Fragment, null, null) if (isTextChild(child)) { const f = createFiber(FiberTag.Text, null, null) f.pp = child f.parent = parent return f } const type = child.type let tag: FiberTag = FiberTag.Host const marker = type && (type as any).$$typeof if (typeof type === 'string') tag = FiberTag.Host else if (type === REACT_FRAGMENT_TYPE) tag = FiberTag.Fragment else if (type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE) tag = FiberTag.Fragment else { // Feature-registered type matchers (Portal, future extractions). Features // that carry the symbol as element.type directly (rather than wrapping in // REACT_ELEMENT_TYPE) match here by type identity. let matched: FiberTag | null = null for (const m of TYPE_MATCHERS) { matched = m(type, marker) if (matched !== null) break } if (matched !== null) tag = matched else if (typeof type == 'function') { tag = type.prototype && type.prototype.isReactComponent ? FiberTag.Class : FiberTag.Function } } const f = createFiber(tag, type, child.key ?? null) f.ref = (child as any).ref ?? null f.pp = child.props f.parent = parent return f } // --------------------------------------------------------------------------- // Reconciliation // --------------------------------------------------------------------------- /** * Reconcile a parent fiber's child list against new normalized children. * Mutates parent.child and the sibling chain. * Mounts new host DOM into `domParent` before `anchor` (or appends if anchor === null). */ export function reconcileChildren( parent: Fiber, newChildren: NormalizedChild[], domParent: Node, anchor: Node | null, ): void { // Fast path: unkeyed positional steady-state. Walk the existing sibling // chain and newChildren in lockstep, validating AND committing in one pass. // On any divergence we fall back to the slow path, which rebuilds the // sibling chain anyway — partial pp writes are idempotent. // Skips the Map / Set / existing-array allocation entirely. if (!currentRoot?.h) { let f: Fiber | null = parent.child let ok = true for (let i = 0; i < newChildren.length; i++) { const child = newChildren[i] if (child == null || !f || f.key != null) { ok = false; break } if (typeof child === 'string') { if (f.tag !== FiberTag.Text) { ok = false; break } f.pp = child } else { if ((child as ReactElement).key != null) { ok = false; break } if (f.type !== (child as ReactElement).type) { ok = false; break } f.pp = (child as ReactElement).props f.ref = (child as any).ref ?? null } f = f.sibling } if (ok && f === null) { // Pass 2: render forward with per-child anchors. Identical to the slow // path's pass 2. for (let r: Fiber | null = parent.child; r; r = r.sibling) { let a = anchor for (let s: Fiber | null = r.sibling; s; s = s.sibling) { const d = firstDomNode(s) if (d && d.parentNode === domParent) { a = d; break } } renderFiber(r, domParent, a) } return } } const existing = collectChildren(parent) const keyed = new Map() for (const f of existing) { if (f.key != null) keyed.set('k' + f.key, f) } let prevNewFiber: Fiber | null = null const claimed = new Set() let structurallyChanged = false // Budget-guided positional matching. We walk `existing` (unkeyed only) with a // single cursor `existingIdx` and, on a type mismatch, choose insert vs delete // based on the remaining length delta (`budget`): // budget > 0: more new than old remain → treat slot as an INSERTION: keep // the old cursor and create a fresh fiber for new[i]. // budget < 0: more old than new remain → treat slot as a DELETION: advance // the old cursor past the mismatched fiber (it'll be um // in the unclaimed pass) and retry. // budget == 0: equal remaining → treat as REPLACE by preferring delete // until budget flips positive or we hit a match. // This avoids greedy forward scans that steal a later same-type fiber for a // newly inserted leading sibling (e.g. smallMenu flipping null →
// stealing the content
's fiber and tearing down the drawer fragment). let existingIdx = 0 let unkeyedOld = 0 for (const f of existing) if (f.key == null) unkeyedOld++ let unkeyedNew = 0 for (const c of newChildren) if (c != null) unkeyedNew++ let budget = unkeyedNew - unkeyedOld // Pass 1 (this loop): match against existing fibers and build the sibling // chain. Pass 2 (after the loop) renders each fiber with the correct // per-child anchor — the firstDomNode of its next still-mounted sibling, // or the parent's own anchor for the rightmost. Without per-child anchors // a child whose render output type changes from no-DOM (Portal, null) to // an in-flow host gets appended to the end of domParent (every child // would otherwise share the parent's anchor) and never moves before its // later siblings. Hit by the t3code Sidebar swap from a portal-rendering // to a
when isMobile flips during a // Provider re-render. for (let i = 0; i < newChildren.length; i++) { const child = newChildren[i] if (child == null) continue let match: Fiber | null = null // key-based match if (child && typeof child === 'object' && !isTextChild(child) && (child as ReactElement).key != null) { const k = 'k' + (child as ReactElement).key const m = keyed.get(k) if (m && m.type === (child as ReactElement).type) { match = m keyed.delete(k) } } if (!match) { while (existingIdx < existing.length) { const cand = existing[existingIdx]! if (claimed.has(cand) || cand.key != null) { existingIdx++ continue } if (sameType(cand, child)) { match = cand existingIdx++ break } // Type mismatch at the cursor. Resolve via budget. if (budget > 0) { // Insertion: leave cand in place, create new for child. break } // Deletion (or replace-as-delete-first): advance past cand. It remains // unclaimed and will be um at the end. existingIdx++ budget++ } } // Detect reorder: matched fiber is not at its original position if (match && existing[i] !== match) structurallyChanged = true let fiber: Fiber if (match) { claimed.add(match) fiber = match if (isTextChild(child!)) { fiber.pp = child } else { fiber.type = (child as ReactElement).type fiber.pp = (child as ReactElement).props fiber.ref = (child as any).ref ?? null } } else { fiber = fiberFromChild(child, parent) structurallyChanged = true if (budget > 0) budget-- } fiber.parent = parent fiber.sibling = null if (prevNewFiber) prevNewFiber.sibling = fiber else parent.child = fiber prevNewFiber = fiber } // Pass 2: walk the sibling chain we just built and render each fiber // forward with the correct per-child anchor. During hydration the cursor // walks DOM forward and each renderFiber adopts the next existing node, // so per-child anchors are moot — fall back to the parent's anchor. const hydrating = !!currentRoot?.h for (let f: Fiber | null = parent.child; f; f = f.sibling) { let a = anchor if (!hydrating) { // Find the firstDomNode of the next still-mounted sibling, if any. for (let s: Fiber | null = f.sibling; s; s = s.sibling) { const d = firstDomNode(s) if (d && d.parentNode === domParent) { a = d; break } } } renderFiber(f, domParent, a) } if (!prevNewFiber) parent.child = null else prevNewFiber.sibling = null // Head content is additive — server may inject metadata/stylesheets (Vite // dev styles, Sentry, analytics) that aren't in the React tree. Unmounting // them on every reconcile thrashes styles and causes flash of unstyled // content. Keep existing head children that weren't matched this pass. const parentIsHeadHost = parent.tag === FiberTag.Host && typeof parent.type === 'string' && (parent.type as string).toLowerCase() === 'head' if (!parentIsHeadHost) { // Unmount unclaimed for (const f of existing) { if (!claimed.has(f)) { unmountFiber(f, domParent) structurallyChanged = true } } // Leftover keyed for (const f of keyed.values()) { if (!claimed.has(f)) { unmountFiber(f, domParent) structurallyChanged = true } } } // During hydration, DOM is already in document order from the cursor-driven // adoption walk. Running placeChildrenInOrder here would reappend nodes to // the end of domParent when the true anchor (often an end marker comment) // isn't reflected in `anchor`. Skip it in hydration mode. // // For , skip always — HeadContent re-renders routinely (route match // changes, providers updating), and reordering every /