Discover how Fiber reconciles the tree, batches updates, and makes rendering work interruptible.
Open this lesson in KodokonSince React 16, the internal engine has been called Fiber: every element in your tree is represented by a fiber node, a unit of work. React actually maintains two trees (a double-buffering technique): the current tree, which reflects what is on screen, and the workInProgress tree, built during rendering. The work happens in two distinct phases: the render phase (calling your components, computing differences), which is interruptible and replayable, then the commit phase (applying mutations to the DOM, running effects), which is always synchronous and uninterruptible. Reconciliation relies on two heuristics that bring the comparison down to linear cost: if an element's type changes at a given position, React destroys the entire subtree and rebuilds it, state included; for lists, the key prop lets React match elements between two renders.
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.The second fundamental mechanism: batching. When several state updates are triggered within the same context, React groups them into a single render. Before React 18, this grouping only worked inside React event handlers; since createRoot, it is automatic everywhere: timers, promises, native listeners. This is also why reading state right after a setState returns the old value: the update is scheduled, not applied.
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.Concurrent rendering pushes the logic further: the render phase can be interrupted, resumed, or even discarded and restarted if a more urgent update comes in. React ranks updates by priority (the lanes): a keystroke is urgent, a list filter marked with startTransition is not. The direct consequence: your component functions must be pure, because React may call them several times without ever committing the result. Another edge case: an external store (Redux, Zustand, a homemade singleton) read during an interruptible render can change mid-render, producing an inconsistent interface known as tearing. The useSyncExternalStore API solves this problem by forcing a synchronous render when the store changes.
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(notify) => {
window.addEventListener('resize', notify);
return () =>
window.removeEventListener('resize', notify);
},
() => window.innerWidth
);
}<div> element becomes a <span> at the same position?createRoot, how many renders do two setState calls inside a setTimeout trigger?