A component renders when its own state changes, when its parent renders, or when a context it reads changes. A change of props is not a trigger by itself: props only change because the parent rendered.
State: a useState setter or useReducer dispatch with a new value. Setting a value that is Object.is-equal to the current one bails out: the children are not rendered (React may still call the component itself once).
Parent render: by default every child renders again, whether its props changed or not. memo (or the React Compiler) is what makes React skip a child with equal props.
Context: every component that reads it with useContext or use renders when the provider's value changes by identity, even inside a memo parent.
External stores: a useSyncExternalStore subscription (Redux, Zustand) renders the component when its selected snapshot changes.
Not triggers: mutating an object or a ref, changing a module variable. React does not watch values; it only reacts to setters.
A render is a function call that produces elements; it touches the DOM only if the result differs.
ReactSenior
Rendering & Reconciliation
What triggers a render, render vs commit, reconciliation and keys, Fiber, concurrent rendering, transitions, Suspense, hydration
Questions closed
0 / 20
- 0Review
- 7Asked often
Questions
A component is the function you write; an element is the plain, immutable object (type, props, key) that JSX creates to describe what to render; an instance is what React keeps for an element mounted at a position in the tree, a fiber that holds its state, hooks and effects.
Elements are cheap and disposable: every render creates new ones; React compares them with the previous ones and throws them away.
Components are called by React, not by you: <Avatar /> creates an element, while calling Avatar() directly runs it as an ordinary function inside the caller, so its hooks become the caller's hooks.
Instances persist between renders as long as the type and key at that position stay the same; that is where useState values live.
Why it matters: state is stored neither in the element nor in the function. It explains why a component declared inside another loses its state (a new type every render) and why an element passed down unchanged as children can be skipped.
In the render phase React calls your components and works out what changed; in the commit phase it applies those changes to the DOM and runs refs and effects. Render must be pure because it can be paused, repeated or thrown away; commit is synchronous and always runs to completion.
Render phase: calls component functions, compares the new elements with the previous tree (reconciliation) and builds a list of changes. Nothing visible happens yet. In concurrent rendering this work can be interrupted by a more urgent update and restarted.
Commit phase: inserts, updates and removes DOM nodes, attaches refs, then runs useLayoutEffect callbacks synchronously before the browser paints.
After commit: the browser paints, and useEffect callbacks run afterwards (passive effects), so they do not block the frame.
Why it matters: side effects in the render body (requests, subscriptions, mutations) may run twice or for a render that never commits. They belong in effects or event handlers.
A render that returns the same output still costs CPU even when the commit has nothing to do.
Parent render, Child render, Child layout effect, Parent layout effect, Child effect, Parent effect. Rendering goes top-down because a parent must run to produce its children; effects run bottom-up during and after the commit, so a parent's effect sees its children already mounted.
Render phase: React calls Parent, gets the <Child /> element, then calls Child. Nothing is in the DOM yet.
Commit phase: the DOM is inserted, refs are attached, then layout effects run child first, synchronously, before the browser paints.
Passive effects (useEffect) run after paint, again child first.
Why child first: a parent's effect may measure or focus a child's DOM node through a ref, so the children must be fully set up before it runs.
**Under StrictMode** in development each render is called twice and each effect runs, cleans up and runs again, so the log is longer but keeps the same order.
Reconciliation compares the element tree of a new render with the previous one to decide which DOM nodes to keep, update or replace. React makes it O(n) with two heuristics: an element of a different type means a different subtree, and key identifies children across renders.
A general tree diff is O(n³); the heuristics trade exactness for speed and hold for almost every real UI.
Different type at the same position (div to section, ProfileA to ProfileB): React unmounts the whole subtree, destroying DOM nodes and state, and mounts a new one.
Same DOM element type: keeps the node and updates only the changed attributes and styles.
Same component type: keeps the instance and its state and renders it with the new props.
Lists: children are matched by key; without keys React matches by index, so inserting at the start makes every item look changed.
State belongs to a position in the tree, not to the component function: {isAdmin ? <Panel /> : <Panel />} keeps the same state in both branches.
The "virtual DOM" is just these element objects; the speed comes from touching the real DOM only where the diff found a change, not from the diff being free.
Once, on mount: Expensive is created by App and reaches Counter as children, so when Counter re-renders, children is the same element object and React skips it. Under StrictMode in development the mount log appears twice, as render is double-invoked.
Clicking sets state in Counter, so only Counter renders; App does not, and the element <Expensive /> it created earlier is reused as is.
When React meets an element with the same type and the very same props object, and no state or context change inside it, it bails out without calling the component.
If Counter rendered <Expensive /> itself, every click would create a new element with new props and Expensive would log on every click.
This "lift content up as children" pattern is the cheapest render optimisation: no memo, no hooks, just moving where the element is created.
React discards the output the component just returned and re-runs it at once with the new state, before rendering its children or committing. It is legitimate only under a condition that stops it, to adjust state when a prop changes; an unconditional call loops into "Too many re-renders".
Only your own state: setting a parent's or another component's state during render triggers the warning "Cannot update a component while rendering a different component".
The pattern: keep the previous prop in state, compare during render, and update both. The children render once, already with the adjusted state.
Why not an effect: useEffect(() => setSelection(null), [items]) commits and paints one frame with the stale selection, then renders the whole subtree again.
Try simpler options first: compute the value during render (selection could be derived from an id), or reset everything with a key.
Concurrent rendering lets React prepare a new UI in the background, pause it to handle something more urgent, and throw it away if it is no longer needed. React 18 turned it on for every app using createRoot, but it only applies to updates you mark as non-urgent.
Opt-in per update: startTransition, useTransition and useDeferredValue mark low-priority work; ordinary setState from a click stays synchronous and urgent.
Not parallelism: JavaScript still runs on one thread. React renders in small slices and yields to the browser between them, so typing stays responsive during a heavy render.
Automatic batching: since 18, updates in promises, timeouts and native handlers are batched too, not only inside React events.
Suspense keeps already shown content on screen during a transition instead of falling back to a spinner, and streaming SSR with selective hydration is built on the same machinery.
Consequences: render may run more than once per commit, so it must be pure; external mutable stores need useSyncExternalStore to avoid tearing.
There is no separate "concurrent mode" any more; ReactDOM.render, the legacy root, was removed in React 19.
An urgent update renders synchronously and commits before the next frame; a transition renders in the background, can be interrupted, and keeps the current UI until it is ready. If the user types mid-transition, React drops that render, commits the keystroke, and restarts the transition.
**isPending** from useTransition is true while the transition is in progress, so you can dim the old content instead of freezing the page.
Suspense inside a transition does not replace already visible content with a fallback; React keeps the old screen until the new one has its data.
Not for controlled inputs: the input's own state must be updated urgently, or typing lags; put only the expensive consequence (filtering, switching tabs) in the transition.
React 19: the callback can be async, which turns it into an Action; isPending stays true until the promise settles. State updates after an await currently need their own startTransition.
A transition does not make the slow component faster; it makes it interruptible.
useDeferredValue(value) returns the previous value during an urgent render and then re-renders in the background with the new one, so a slow part of the UI lags behind fast input without blocking it. Use it for expensive rendering; use debouncing when the cost is a network request.
No fixed delay: on a fast device the deferred render finishes almost at once, on a slow one it waits; a debounce waits its 300 ms everywhere.
Interruptible: each new keystroke abandons the stale background render, so you never wait for intermediate values to finish.
**Pair it with memo on the slow child: without it the child still renders with the old value during the urgent pass and nothing is saved.
Stale indicator**: compare value !== deferredValue to dim outdated results.
Network: deferring does not reduce the number of requests; a debounce (or TanStack Query with a debounced key) does.
React 19 adds an initialValue argument, used for the first render before the real value is deferred.
A component suspends when it reads something still loading: React stops rendering that subtree, shows the closest Suspense boundary's fallback, and retries the render when the awaited promise resolves. React 19 commits that fallback right away and pre-renders the suspended siblings afterwards.
What suspends: lazy while its chunk loads, use(promise) until it settles, Suspense-enabled libraries such as TanStack Query's useSuspenseQuery, async Server Components. A fetch in useEffect does not.
Nothing is kept from a suspended first render: on retry React renders the tree from scratch, so the promise must come from a cache, a loader or a framework. One created during render is new every time and never settles in time.
Content that was already visible: if an urgent update makes it suspend, React hides it (keeping its state) and shows the fallback; inside a transition it keeps the old content on screen instead.
React 19 change: before, React rendered every sibling of the suspended component before committing the fallback; now the fallback commits immediately and the siblings are pre-warmed in a separate render, so their lazy requests still start early.
Errors: a rejected promise goes to the nearest error boundary, not to Suspense.
Hydration is when React on the client takes server-rendered HTML, builds its component tree over the existing DOM and attaches event handlers instead of recreating the DOM. Selective hydration hydrates each Suspense boundary on its own, starting with the one the user interacts with.
Entry point: hydrateRoot(document.getElementById('root')!, <App />) instead of createRoot.
Before React 18: hydration was all or nothing; the whole page had to download its JS, render and hydrate before any button worked.
Streaming SSR: renderToPipeableStream (Node) or renderToReadableStream (Web streams) send the shell first and each Suspense boundary's HTML when its data is ready.
Selective hydration: React hydrates boundaries independently; if the user clicks inside one that is not hydrated yet, React raises its priority, hydrates it first and replays the click.
The contract: the first client render must produce the same output as the server. Reading window, Date.now() or localStorage during render breaks it and causes a hydration mismatch; move such reads into an effect.
Fiber is React's reconciler since version 16: each element instance becomes a fiber node, a unit of work React can pause, resume or discard. The old stack reconciler recursed through the whole tree in one synchronous call, so a big update blocked input until it finished.
What a fiber is
A node per component instance or host element, holding its type, props, state (the hook list), effects and pointers to child, sibling and return (parent).
A linked list instead of the call stack: React walks the tree with a loop it controls, so it can stop after any fiber and continue later.
Double buffering: the current tree is what is on screen; updates build a workInProgress tree linked through alternate, and the commit swaps them.
What it made possible
Interruptible rendering: concurrent features (transitions, useDeferredValue, Suspense) all depend on yielding between fibers.
Priorities: each update carries a lane; React works on urgent lanes first and can drop half-finished low-priority work.
Error boundaries, fragments, portals and returning arrays arrived with Fiber because the new tree could represent them.
In interviews, the key point is the trade: render work became interruptible, which is why render must be pure and why commit stays synchronous.
React walks the new children left to right, reuses the fiber with the same key, and moves a DOM node only when its item now comes after one that used to follow it. Moving the last item to the front is the worst case: React keeps that item in place and moves every other node after it.
The algorithm
First pass: compares old and new children at the same index while the keys match; appends and removals at the end are settled here cheaply.
Map pass: at the first mismatch, the remaining old fibers go into a Map by key, and each new child takes its old fiber (state and DOM) if the type matches.
Moves: React tracks the highest old index reused so far (lastPlacedIndex); a reused item with a lower old index is moved, the others stay. Old fibers left in the map are deleted.
There is no longest-increasing-subsequence step (Vue 3 and Inferno have one), so React trades minimal moves for a simple single pass.
Consequences
Last item to the front moves n - 1 nodes; first item to the end moves one.
Index keys turn a reorder into "every item got new props": no moves, but every row re-renders and state stays attached to the wrong row.
Unstable keys (Math.random(), ids generated during render) remount every item on every render.
For long lists that reorder often, virtualization (fewer nodes) helps more than tuning the order.
Every update is assigned a lane, a bit in a bitmask that encodes its priority, and React always renders the most urgent pending lanes first. Non-urgent lanes are rendered in slices of about 5 ms, after which React yields to the browser and checks whether something more urgent arrived.
Lanes
Priority comes from where the update happens: a click or keypress gets the synchronous lane, continuous events like scroll or pointer move get a higher-than-default lane, ordinary updates get the default lane, and startTransition assigns one of the transition lanes. Idle and offscreen work sit at the bottom.
Bitmask, not a single number: React can batch several lanes into one render and track which ones are pending, suspended or already done.
Starvation: a low lane that keeps getting interrupted is eventually marked expired and rendered synchronously, so a transition cannot be postponed forever.
Time slicing and interruption
Sync lane: rendered in one go without yielding, which is why urgent updates feel instant.
Concurrent lanes: the work loop checks shouldYield() between fibers; the Scheduler package posts the next slice through a MessageChannel task so input events get a chance to run.
Interruption: if an urgent update lands mid-transition, React drops the half-built workInProgress tree, renders and commits the urgent update, then restarts the transition on top of the new state.
You rarely touch any of this directly; it explains why a transition can be rendered several times and why render-phase side effects are dangerous.
A signal is a reactive value that records who reads it: when it changes, only the effects and DOM bindings that read it update, and the component function does not run again. React instead re-runs the component with a new state snapshot and diffs the output, trading extra work for a simpler model.
How signals work
Tracking: reading count() in Solid or Angular, or count.value in Preact, inside an effect or a template subscribes that code; a write notifies exactly those subscribers.
Components run once in Solid: the function builds the DOM and its bindings, and later updates go straight to a text node or an attribute, with no virtual DOM diff.
The same idea drives Vue's ref, Svelte 5 runes and MobX, and a TC39 proposal aims to add signals to JavaScript itself.
Why React keeps re-rendering
Plain JavaScript in render: state values are ordinary variables, so destructuring, early returns and any expression just work. Signals come with tracking rules: in Solid, destructuring props or reading a signal outside a tracked scope silently loses reactivity.
A render sees one snapshot: every value is fixed for that render, which is what lets concurrent rendering prepare a new UI in the background and throw it away. Shared mutable signals make that consistency much harder.
The cost is limited another way: memoization skips unchanged subtrees, and the React Compiler now inserts it automatically, which narrows the gap without changing the model.
In practice
Fine-grained updates win on UIs with many tiny, frequent changes (a live dashboard, a spreadsheet grid); React wins on ecosystem and on code that reads top to bottom.
Signal-style stores such as Jotai or MobX plug into React, but they narrow which components re-render rather than skip rendering.
During a transition React refuses to hide content that is already visible: if the new screen suspends inside a boundary that is already showing content, React keeps the old UI (with isPending true) until the data arrives. Boundaries that are mounting for the first time still show their fallbacks.
The rules
Urgent update that suspends: the nearest boundary switches to its fallback at once, even if it was showing content. That is the "page flashes to a spinner" effect.
Transition that suspends an already revealed boundary: React waits and keeps the old content; routers (React Router, Next.js) wrap navigations in transitions for this reason.
New boundaries inside the incoming screen are allowed to show fallbacks, so the shell renders and the slow parts stream in.
Controlling the experience
Place boundaries by design: one around the page shell, inner ones around independently slow widgets. Too many boundaries make the page pop in like popcorn; React throttles reveals (about 300 ms) to group them.
Force a fresh fallback for content that is a different thing, not an update: a key on the boundary, such as <Suspense key={productId}>, makes it a new boundary.
Show progress during the wait: use isPending (or the router's navigation state) to dim the old page or show a top progress bar.
Avoid: wrapping the input that triggers the search in the transition, or the field stops responding while the results suspend.
setTab runs after the await, outside the synchronous scope of startTransition, so React treats it as an ordinary urgent update. An urgent update that makes Posts suspend hides the tab already on screen and shows the fallback; wrap the update after the await in its own startTransition.
Why the mark is lost: React tags updates as a transition only while the callback runs synchronously. After an await the code resumes in a later microtask, and JavaScript cannot carry that context there yet; the React docs expect the AsyncContext proposal to remove the limitation.
What the user sees: isPending is true during the await; then the urgent setTab commits at once, the old tab gets display: none, the spinner appears, and isPending is already back to false while the spinner is showing.
With the fix the update joins the transition: React keeps the old tab visible with isPending still true until Posts has its data, then swaps in one commit.
**Every await resets it**: an action with several awaits needs startTransition around each state update that follows one.
**useActionState applies the return value for you**: returning the new state from the action, instead of calling a setter after the await, commits it inside the transition.
flushSync(fn) from react-dom renders and commits the updates made inside fn synchronously, so the DOM is updated when it returns. You need it only when imperative code right after the update must see the new DOM: scrolling to a new item, measuring it, or passing it to a non-React library.
Normal behaviour: updates are batched and committed after the handler finishes, so listRef.current.lastChild right after setItems still shows the old list.
Typical uses: scroll or focus to a newly rendered element, the browser's beforeprint event (the print snapshot is taken synchronously), integration code that reads layout immediately.
Cost: it defeats batching and concurrent rendering for that update, and it can force pending Suspense boundaries to show their fallbacks.
Do not call it from render or from an effect: React warns ("flushSync was called from inside a lifecycle method") and cannot flush while already rendering.
Often an effect or a ref callback on the new element is the cleaner alternative.
React unwinds to the nearest error boundary and renders its fallback; with no boundary it unmounts the whole root and the page goes blank. React 19 reports every such error once through root options: onCaughtError when a boundary handled it, onUncaughtError when none did.
Retry first: if the error happens in a concurrent render, React retries the tree once synchronously before treating it as real, in case it came from an inconsistent intermediate state.
**onRecoverableError: errors React fixed by itself, mostly hydration mismatches where it fell back to client rendering. Log them: they cost performance and point to server and client output that differ.
One log per error**: React 18 re-threw caught errors and they appeared twice in the console; React 19 logs each once and passes errorInfo.componentStack to the callbacks.
Where to send them: to Sentry or your logging service from these callbacks, which gives one report per error with its component stack.
What never reaches them: errors in event handlers and in async code after render. They are ordinary JavaScript errors (window.onerror, unhandledrejection).