Back to Blog
ReactJune 21, 20264 min read

React Rendering: How State Updates and Reconciliation Work

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.

ReactFrontend EngineeringWeb DevelopmentJavaScriptPerformanceNext.jsTutorial
Coding on a laptop outdoors, showcasing a rooftop urban lifestyle in Surat, India.

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."

The Trigger: State Updates

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.

The Render Phase and Reconciliation

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:

  1. Old Tree: <div><button>Click</button></div>
  2. New Tree: <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.

The Commit Phase: Touching the DOM

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.

A Practical Example

Consider a simple counter component:

JAVASCRIPT
function 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:

  1. setCount is called.
  2. React schedules a re-render for Counter.
  3. The function runs, returning the new JSX.
  4. React compares the new object with the old one.
  5. React realizes only the text node {count} needs to change.
  6. The browser DOM is updated with the new number.

Common Pitfalls

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.

FAQ: Tracing React Rendering

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.

Final Thoughts

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.

Similar Posts