React rendering follows a two-pass mental model involving state batching and reconciliation. Learn how React Fiber manages the render and commit phases.
Last month, I spent about three hours debugging a performance bottleneck in a dashboard component. Every time a user toggled a filter, the entire UI flickered, and the browser locked up for roughly 180ms. It wasn't a complex app, but my misunderstanding of how React processes updates was causing unnecessary work.
If you’re still thinking of React updates as "change state, UI updates immediately," you're going to hit a wall. To write performant code, you have to adopt the "Two-Pass" mental model: the Render phase and the Commit phase.
When you call setState in React, nothing happens to the DOM right away. React doesn't just push pixels to the screen; it schedules a task. This is where React rendering becomes interesting.
The process splits into two distinct stages:
We first tried to fix my dashboard performance issue by wrapping everything in memo, but that didn't help. We were triggering too many renders because we didn't account for how React groups these updates.
React doesn't perform a render pass for every single setState call. If you have a function that updates state three times in a row, React batches those updates into a single render cycle.
JAVASCRIPTconst handleClick = () => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); // React batches these. // It only re-renders once after this function finishes. };
This is a massive performance win. In earlier versions of React, batching was limited, but since React 18, it’s automatic even inside promises, timeouts, and native event handlers. Understanding this React rendering behavior is crucial because it prevents your application from doing redundant work.
Under the hood, this two-pass system is powered by React Fiber. Think of Fiber as a specialized engine that can pause, resume, and abandon work.
Because the Render phase is pure, React can "interrupt" it. If a higher-priority task comes in—like a user typing in an input field—React can drop the current Render phase, handle the input, and then restart the work on your dashboard.
When you dig into the React rendering lifecycle, you'll see why keeping your render logic "pure" is non-negotiable. If you perform side effects (like API calls or random number generation) directly in your component body, React might run that code multiple times without ever committing it to the screen.
Once the Render phase finishes, React has a "diff" of what changed. This is the heart of reconciliation. React uses a tree-diffing algorithm to determine the minimum number of operations required to update the DOM.
If you’re working with lists, you’ve likely seen the warning about key props. React keys and reconciliation are the primary way React tracks identity during this pass. If your keys are unstable (like using Math.random()), React can't track elements correctly, leading to massive re-renders that ruin your performance.
If you're feeling overwhelmed, here’s my advice:
fetch or complex calculations directly in the component body. Use useEffect or useMemo.I’m still not perfect at this. Sometimes I’ll still write a component that re-renders more than it should, and I have to go back in with the React DevTools Profiler to figure out why. The goal isn't to write "zero render" code; it's to write code where you understand why the browser is painting what it’s painting.
Next time you’re debugging, ask yourself: "Is this code in the render phase or the commit phase?" It changes everything.
Does React batch state updates in setTimeout?
Yes. Since React 18, batching is automatic regardless of where the state update is triggered, including setTimeout, fetch callbacks, and native event listeners.
Can I stop a re-render from happening?
You can use React.memo, useMemo, or useCallback to prevent components from re-rendering if their props haven't changed. However, always profile your app first—premature optimization often adds more complexity than it solves.
What happens if I trigger a state update during the render phase?
React will throw an error or enter an infinite loop. You should never update state directly inside the function body of a component. Always trigger state updates inside event handlers or useEffect.
React useEffect is for synchronizing external systems, not state management. Learn why treating it as a lifecycle method leads to bugs and how to fix it.