React rendering and state updates can feel like magic. Learn how reconciliation and the virtual DOM actually work to keep your UI in sync with your data.

When I started with React, I treated the component lifecycle like a black box. I’d call setState, the screen would update, and I was happy enough. But then I hit a production bug where a list of 500 items caused a noticeable 300ms jank every time a user typed a character. I realized that if I didn't understand how React rendering actually worked under the hood, I was just guessing.
To get a handle on this, you have to stop thinking about "updating the DOM" and start thinking about "describing the UI."
Everything begins with a trigger. In React 18+, whether it’s a useState hook, a useReducer dispatch, or a parent component re-rendering, the process is the same. React marks the component as "dirty."
It’s tempting to think this triggers an immediate DOM mutation. It doesn't. Instead, React schedules an update. It’s a subtle but vital distinction. If you call setCount five times in a single event handler, React isn't going to run your component function five times instantly. It batches these updates to keep your app fast.
Once the update is scheduled, React enters the Render Phase.
During the render phase, React calls your component function. It doesn't touch the real DOM yet. Instead, it generates a new tree of React elements—a snapshot of what you want the UI to look like based on the new state.
This is where the React virtual DOM comes in. React compares this new tree with the previous one. This comparison process is what we call reconciliation.
Think of it like this:
<div><button>Click</button></div><div><button>Clicked</button></div>React sees that the div is the same, but the text inside the button changed. It doesn't throw away the button element and recreate it. It just updates the text node. This is the "diffing" algorithm in action. By minimizing changes to the real DOM, React keeps your app responsive, even when your state tree is complex. If you want to dive deeper into why these updates happen in the first place, check out my guide on the React Rendering Lifecycle: Why Components Re-render and How to Optimize.
After reconciliation, React enters the Commit Phase. This is the only time React actually interacts with the browser's DOM. It applies the specific changes identified during the diffing process.
I’ve seen junior devs accidentally force full re-renders by misusing keys in lists. If you use index as a key for a list that changes order, React gets confused during reconciliation. It thinks the existing DOM nodes are still the right ones for the new data, leading to state bugs in child components. Always use stable, unique IDs.
Consider a simple counter component:
JAVASCRIPTfunction Counter() { const [count, setCount] = useState(0); console.log("Rendering Counter"); return ( <button onClick={() => setCount(c => c + 1)}> Count: {count} </button> ); }
When you click the button:
setCount is called.Counter.{count} needs to change.One major mistake I made early on was putting heavy calculations directly inside the component body. Since the component re-renders every time state changes, that calculation runs every time, too.
If you’re doing heavy work, use useMemo. It tells React: "Only re-run this logic if these specific dependencies change." It’s an essential tool for keeping your render phase lean. Also, if you're dealing with complex state structures, don't forget that using the React Context API Guide: Solving State Management Without Bloat can help you avoid unnecessary re-renders by decoupling state from component props.
Q: Does every state update lead to a DOM update? A: No. If your render function returns the exact same JSX as before (the result of the reconciliation is "no change"), React will skip the commit phase entirely.
Q: Why does my component re-render twice in development?
A: React 18's StrictMode deliberately renders components twice to help you find side-effect bugs. It’s not a bug; it’s a feature meant to help you write cleaner code.
Q: How do I know if my reconciliation is slow? A: Use the React DevTools Profiler. It shows you exactly which components re-rendered and why. If you see a component rendering in 5ms but it’s happening 100 times, that’s your bottleneck.
I’m still learning how React’s concurrent features change the way we think about these renders. As we move toward more server-centric patterns, the boundary between "render" and "fetch" is getting blurrier. Don't stress if the mental model takes time to click—it took me months to stop seeing the DOM and start seeing the state tree.
Next time you're debugging, try adding a console.log at the top of your component. Watch the console as you interact with the app. You’ll be surprised how often things render, and that curiosity is exactly what makes a great engineer.
React state management gets easier when you learn how to lift state up. Discover how to sync sibling components and build a predictable data flow today.
Read more