David Dunn

Senior Software Developer


Back to Blog

Rendering vs Committing: What Actually Happens in React

7 min read
react

A practical mental model for React’s render pipeline, and why 're-render' isn’t always the performance concern we may think it is.

function Counter() {
  const [count, setCount] = useState(0);
  console.log("render", count);

  const onClickHandler = () => setCount((prevCount) => prevCount + 1);

  return <button onClick={onClickHandler}>{count}</button>;
}

Using the famously simple counter example above we can see that this component would log out render <count> every time we click the button. So:

render 0
render 1
render 2
render 3
...

A common concern here is that the component is rendering every time we click the button and the common solution would be to use something like useMemo with React.memo to prevent unnecessary but here we need to take a moment to think, is this really an issue? Or is React just doing its job?

To know the answer to that question, we need to understand what that job is. For that, we should know that React has three main phases:

So really a re-render is just React through the render phase again and ‘thinking’ about what the UI should look like.

The Trigger Phase - Where is all starts

The most common actions that trigger a re-render are:

It is important to remember that this only triggers the render phase, if no value was actually changed then no changes to the DOM would be made.

The Render Phase - Deeper dive

When a render a triggered React calls the components to ask: “Given these props and this state, what should the UI look like now?”

In technical terms that means:

At high level, React keeps roughly two versions of our UI in memory:

The process of building these trees, comparing each component/fiber created, and building an effect list is called reconciliation. It is an integral part of the render phase.

The effect list is a list of ‘things that need to change in the DOM later’.

It is important to note:

So with this knowledge we can better understand what kind of operations to include or omit from the body of our React components.

For example, it’s not a good idea to perform things like:

This is why each of those are typically done in useEffect hooks or memoized computations.

In some cases these operations would be even earlier, like server cache for example.

Once React has calculated what changes need to be made it moves onto the commit phase.

The Commit Phase - When changes actually happen

This is where React will:

How does React do this?

Well first it uses the effect list it built in the render phase. This is a list of all the DOM changes that React calculated needed to happen. Once the list has been complete React will then execute any useLayoutEffect hooks, then the browser paint happens and finally the useEffect hooks.

## So.. Is a re-render bad or good?

Well, it depends. A re-render happens when React calls our component function again during the render phase because some props or state changed.

But this DOES NOT mean that:

The commit phase only occurs if React actually calculated changes need to happen.

So if we keep our components well organized and keep expensive computations memoized, network calls in effects etc etc, the render phase is actually very quick and the user of our application would never know it even happened.

In these instances, a re-render is NOT a bad thing.

That doesn’t mean that all re-renders are ok though, it is important to understand when a re-render is causing issues and some common things we can look out for are:

In these instances a re-render may be causing an issue in our application.

So how do we fix it?

High Level Optimization Toolbox

React.memo

const Row = React.memo(function Row({ item }: { item: Item }) {
  return <div>{item.name}</div>;
});

Using React.memo allows us to skip the re-render of a pure component when its props haven’t changed.

useMemo

const filtered = useMemo(() => items.filter((item) => item.visible), [items]);

We can use the useMemo hook to cache the result of an expensive calculation. By providing a list of dependencies we can instruct React to only recalculate the value if one of those dependencies change.

useCallback

import React, { useCallback } from "react";

type Todo = { id: number; text: string; done: boolean };

const TodoItem = React.memo(function TodoItem({
  todo,
  onToggle,
}: {
  todo: Todo;
  onToggle: (id: number) => void;
}) {
  console.log("render <TodoItem />", todo.id);
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={todo.done}
          onChange={() => onToggle(todo.id)}
        />
        {todo.text}
      </label>
    </li>
  );
});

export function TodoList({ todos }: { todos: Todo[] }) {
  // ✅ Stable function identity between renders
  const handleToggle = useCallback((id: number) => {
    console.log("toggled", id);
  }, []); // deps: [] → function never changes

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
      ))}
    </ul>
  );
}

Splitting Context and/or state

const AppContext = createContext<AppState>(/* everything */);

App wide context and state is generally a bad idea and can cause the entire tree, which is the whole app, to re-render whenever it changes.

Instead we should keep context and state as local as possible.

### The DevTools profiler

We should always aim to provide evidence for performance optimizations. By using the profiler we can clearing see how much time particular components take to render/commit. This gives us a baseline performance number to work with. Then after we apply our optimizations we can run the profiler again and measure whether our changes had the intended affect or not.

Conclusion

A re-render just means React re-ran that computation. That is completely normal, and is just React doing its job.

Performance problems don’t come from the existence of re-renders; they come from:

If the UI is smooth and our components are simple and pure, we don’t need to fear re-renders.

We shouldn’t fear re-renders; we should fear doing too much inside them.