What Is Reconciliation in React?
Advertisement
🧩 Scenario
Architecture Walkthrough
Render Phase: Computing the New Tree
Reconciliation begins when something triggers a re-render: a useState setter call, a new prop value from a parent, a context value change, or a useReducer dispatch. React calls the affected component function (or calls render() for a class component) to produce a new description of the UI as a virtual DOM tree (a tree of React elements).
This rendering is a pure computation. It has no side effects on the DOM. React compares the new tree against the previous tree using the diffing algorithm to determine the minimal set of changes needed. This comparison happens entirely in memory on the virtual representation, not on the real DOM.
Crucially, a component re-rendering (its function being called) does not mean the DOM changes. If the output is identical to the previous render, the commit phase may have nothing to do.
Commit Phase: Applying DOM Changes
After the render phase computes the diff, the commit phase applies the changes to the real DOM. This is when document.createElement, attribute mutations, and node removal actually happen. React runs useLayoutEffect cleanup and setup during commit. useEffect cleanup and setup are scheduled to run asynchronously after the browser has painted.
The commit phase cannot be interrupted. React completes it in one synchronous pass to ensure the DOM is never left in a partially updated, inconsistent state.
Fiber and Concurrent Rendering
Before React 16, reconciliation ran synchronously and could not be interrupted. Large renders would block the main thread for the entire duration. React 16 rewrote the reconciler using the fiber data structure. A fiber is a lightweight object that represents a unit of work for one component. The fiber tree mirrors the component tree.
In React 18's concurrent mode, the render phase is interruptible. React processes fibers one at a time and can yield control to the browser between fibers if higher-priority work (user input, animations) arrives. The interrupted render is discarded or paused and resumed from where it left off. The commit phase still runs synchronously to completion once the render phase is done.
Key Code Explained
// What triggers reconciliation
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// React 18: batches both updates into a single re-render
// (React 17 only batched inside event handlers)
const handleClick = () => {
setCount((c) => c + 1); // schedules update
setName('updated'); // schedules update
// React batches both → ONE render cycle, not two
};
// Even async code is batched in React 18
const handleAsync = async () => {
const data = await fetchData();
setCount((c) => c + 1); // React 18: still batched
setName(data.name); // React 18: still batched → ONE render
};
return <Child count={count} name={name} />;
}
// Child re-renders when parent re-renders, even with same props
function Child({ count, name }: { count: number; name: string }) {
console.log('Child rendered'); // this runs on every parent re-render
return (
<div>
{count} - {name}
</div>
);
}
// React.memo prevents re-render when props are shallowly equal
const MemoizedChild = React.memo(Child);
// Now Child only re-renders when count or name actually changes
// Reconciliation phases in pseudo-code
function reconcile(current: Fiber, newElement: ReactElement) {
// RENDER PHASE (interruptible in concurrent mode)
const workInProgress = createWorkInProgressFiber(current);
callComponentFunction(workInProgress); // run useState, useEffect registrations
diffWithPrevious(workInProgress); // compare old and new output
// COMMIT PHASE (synchronous, not interruptible)
applyDOMChanges(workInProgress); // create/update/delete DOM nodes
runLayoutEffects(workInProgress); // useLayoutEffect setup
schedulePassiveEffects(workInProgress); // useEffect runs after paint
}
// useLayoutEffect vs useEffect timing in commit phase
function Modal({ isOpen }: { isOpen: boolean }) {
const ref = useRef<HTMLDivElement>(null);
// Runs synchronously in the commit phase, before the browser paints.
// Use for measuring DOM and applying corrections before the user sees the frame.
useLayoutEffect(() => {
if (isOpen && ref.current) {
const rect = ref.current.getBoundingClientRect();
// Apply position corrections before paint — no visible flash
}
}, [isOpen]);
// Runs asynchronously after the browser has painted.
// Use for subscriptions, event listeners, data fetching.
useEffect(() => {
if (isOpen) {
fetchModalData();
}
}, [isOpen]);
return isOpen ? <div ref={ref} className="modal">...</div> : null;
}
The batching behavior change in React 18 is significant: in React 17, calling two setters inside a setTimeout or async function would cause two separate renders. React 18 batches all updates regardless of where they originate. Use flushSync from react-dom to opt out of batching if you need a synchronous DOM update between two state changes.
Tradeoffs
| Phase | Interruptible (React 18) | DOM side effects | Timing |
|---|---|---|---|
| Render phase | Yes (concurrent mode) | None | During rendering, may be paused |
| Commit phase | No | Yes | Synchronous, after render is done |
| useLayoutEffect | No (in commit) | Can read DOM | After commit, before browser paint |
| useEffect | No | Can read DOM | After browser paint (async) |
What Interviewers Actually Check
- Whether you know reconciliation has a render phase and a commit phase
- Whether you know the render phase is now interruptible in React 18 and why that matters
- Whether you know that a component re-rendering does not always mean DOM mutations
- Whether you know what batching is and how React 18 extended it to async code
- Whether you know the timing difference between
useLayoutEffectanduseEffect
Follow-Up Questions
- What is
startTransitionin React 18 and how does it mark work as low-priority to keep the UI responsive? - How does React's concurrent mode use the scheduler to prioritize user input over background rendering?
- What is Suspense and how does it interact with the render phase to show fallback UI during data loading?
- If a component throws during the render phase, how does React handle it?
- What is the purpose of
useIdand how does it relate to server-side rendering and hydration?
Common Candidate Mistakes
- Saying React "diffs the real DOM" when reconciliation happens on virtual DOM trees in memory
- Saying every state update immediately produces a DOM mutation (React batches and defers commit)
- Conflating the render phase (component function runs) with the commit phase (DOM mutates)
- Not knowing React 18 extended batching to async code, only knowing about batching inside event handlers
- Not knowing that the commit phase is always synchronous, even in concurrent mode
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain the render phase (virtual DOM computation) and commit phase (DOM updates) and what distinguishes them?
- Can you describe what triggers reconciliation?
- Can you explain how fiber enables React 18 to pause and resume render work?
- Can you describe what batching means and how React 18 extended it?
- Can you explain why a component re-render does not always produce a real DOM change?
Summary
Reconciliation is the process by which React determines what needs to change in the real DOM when application state changes. It has two phases. The render phase calls component functions to produce a new virtual DOM tree and compares it against the previous tree using the diffing algorithm. This phase is pure computation with no DOM side effects and is interruptible in React 18's concurrent mode. The commit phase applies the computed changes to the real DOM, runs useLayoutEffect, and schedules useEffect. The commit phase always runs synchronously to completion.
A key conceptual distinction is that re-rendering (a component function being called) is not the same as a DOM update. A component may re-render and produce output identical to its previous output, in which case the commit phase has no DOM mutations to perform. React minimizes DOM mutations because they are expensive relative to JavaScript computation.
React 18 extended automatic batching to all update sources, including setTimeout, promises, and other async contexts. This means multiple state setters called in sequence produce a single render cycle rather than one render per setter call. The flushSync API opts out of batching when a synchronous DOM update is required between two state changes.
Is reconciliation and diffing the same thing?
Diffing is the comparison step within reconciliation. Reconciliation is the broader process: React renders the component tree, diffs the output against the previous tree, computes the minimum set of DOM mutations, and commits those changes to the real DOM.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement