# How React Actually Renders (and Why Your App Re-renders Too Much)

![](https://cdn.hashnode.com/uploads/covers/69c222b930a9b81e3afd3695/328c5e64-2e8b-4cf7-93cf-e3e10072e493.png align="center")

I spent two days last week staring at the React Profiler, wondering why a simple filter dropdown was causing 47 components to re-render. Forty-seven. For a dropdown.

That's when I stopped just *using* React and started actually understanding how it works under the hood. Turns out, a lot of the "mysterious" performance issues make complete sense once you get the rendering model. Let's break it down.

**First — what even is a "render"?**

In React, a render is just React calling your component function. That's it. When React renders `<UserCard />`, it literally runs the `UserCard` function and looks at what JSX came out.

People think rendering = DOM update. It doesn't. Rendering is React running your function. Updating the DOM is a separate step that only happens if something actually changed.

This distinction matters a lot.

**What triggers a re-render?**

Three things cause React to re-render a component:

1.  **State changes** — you call setState or dispatch to a reducer. React re-renders that component and everything below it in the tree.
    
2.  **Props changes** — when a parent re-renders, all its children re-render too. Even if the child's props didn't change. Yes, really.
    
3.  **Context changes** — if a useContext value updates, every component consuming that context re-renders. The diagram above shows the full flow. Notice there's no "props actually changed" check by default — React just re-renders the whole subtree. This is where most performance issues come from.
    

**The render, reconcile, commit cycle**

Once React decides to render, here's what happens:

**Render phase** — React calls your component function and builds a new virtual DOM tree. This is pure JavaScript, nothing touches the browser yet.

**Reconciliation** — React compares the new virtual DOM with the previous one. This is the "diffing" step. React figures out the minimum set of changes needed.

**Commit phase** — React applies those changes to the actual DOM. This is the expensive step, but React tries to minimize it.

The key insight: just because React *renders* your component doesn't mean it *updates* the DOM. If nothing visually changed, the commit phase does nothing. Renders are cheap. DOM updates are expensive.

**The re-render problem in real life**

Here's a pattern I see everywhere — and had in my own code:

```tsx
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Click me</button>
      <ExpensiveChild />  {/* re-renders every time count changes */}
    </div>
  );
}
```

Every time you click the button, `ExpensiveChild` re-renders — even though it has nothing to do with `count`. React doesn't know that. It just sees "parent rendered, re-render all children."

### Fix 1 — `React.memo`

Wrap the child in `React.memo` and React will skip re-rendering it if its props haven't changed:

tsx

```tsx
const ExpensiveChild = React.memo(() => {
  return <div>I only render when my props change</div>;
});
```

Now clicking the button won't touch `ExpensiveChild` at all. But there's a catch — if you're passing a function or object as a prop, this breaks down fast.

### Fix 2 — `useCallback` for stable function references

tsx

```tsx
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // stable reference across renders

  return <ExpensiveChild onClick={handleClick} />;
}
```

Without `useCallback`, `handleClick` is a brand new function object on every render. `React.memo` sees a new prop value and re-renders anyway. `useCallback` gives you the same function reference between renders so the memo check actually works.

### Fix 3 — `useMemo` for expensive calculations

tsx

```tsx
const filteredList = useMemo(() => {
  return hugeList.filter(item => item.active);
}, [hugeList]);
```

Don't run this on every render. Memoize it and only recalculate when `hugeList` actually changes.

### When NOT to memoize

This is the part nobody talks about — **memoization has a cost too.**

React has to store the previous value, run the comparison, and decide whether to use the cached result. For simple components and cheap calculations, this overhead is often *more* expensive than just re-rendering.

My rule of thumb:

*   `React.memo` → only for components that render often AND are expensive to render
    
*   `useMemo` → only for genuinely expensive calculations (think filtering/sorting 1000+ items)
    
*   `useCallback` → mostly when passing callbacks to memoized children or as `useEffect` deps
    

Don't sprinkle these everywhere **"just in case." Profile first, optimize second.**

### How to actually find the problem — React Profiler

Stop guessing. Open React DevTools, go to the Profiler tab, hit record, interact with your app, and stop recording.

You'll see a flamegraph showing exactly which components rendered and how long each one took. The bars that are wide and re-render repeatedly are your targets.

I found my 47-component re-render problem in about 3 minutes with the Profiler. Turned out a context value was being recreated on every render because the object wasn't memoized. One `useMemo` fixed it.

### The mental model to remember

React rendering is a **pull** system, not a push system. When state changes, React doesn't surgically update just the affected component — it re-runs the entire subtree from that point downward, then figures out what actually needs to change in the DOM.

Your job as a developer is to give React hints — via `memo`, `useCallback`, `useMemo` — about what *hasn't* changed so it can skip work.

But don't optimize blindly. Profile first, find the actual bottleneck, then apply the right fix.

### What's next

Day 3 is going to be on **component design patterns** — specifically compound components, render props, and custom hooks, and when to reach for each one. These patterns come up a lot in system design interviews so worth understanding properly.

Drop a comment if the Profiler thing clicked for you — or if you've had a wild re-render bug I should know about.
