In-Depth Article
How React Updates Work: What Happens Between setState and a DOM Update?
Understand how React turns setState into a DOM update through batching, scheduling, the render phase, the commit phase, and effect timing.
When people first learn React, they usually remember one simple idea: when state changes, the UI changes too.
That idea is correct, but it leaves out most of what actually matters.
Inside React, a state change does not immediately become a DOM change. Instead, React breaks the whole process into several steps:
- It receives the state update
- It decides how urgent that update is
- It calculates what the next version of the UI should look like
- Only then does it commit the result to the real DOM
So a more accurate way to think about React is this:
React does not "update the page immediately." It manages a full pipeline from state change to UI commit.
Once you understand that pipeline, a lot of React behavior starts making more sense:
- Why multiple
setStatecalls may not cause multiple renders - Why some components do not re-run
- Why
useLayoutEffectanduseEffectrun at different times - Why React can support priorities and interruptible rendering
This article walks through that full chain from start to finish.
If you want deeper background on related internals, see why Hooks must keep a stable call order, how concurrent rendering is built, and why useEffect and useLayoutEffect differ in the commit phase.
1. Start with the right mental model
Before talking about terms like Trigger, Render, or Commit, it helps to answer one core question first:
Why does React not update the DOM the moment you call setState?
Because doing that would be too blunt and too expensive.
If React synchronously updated the DOM every single time state changed, several problems would show up immediately:
- Consecutive updates would repeat work unnecessarily
- Updates could not be batched together
- There would be no real way to distinguish urgent work from non-urgent work
- React would have a harder time skipping parts of the tree that do not need to change
- Large updates would hurt interactivity
So React's first reaction is not "change the DOM right now." Its first reaction is closer to this:
Record the update first, then decide the most efficient way to process it.
That is the real starting point of React's update model.
2. What does setState actually trigger?
When you call setState or dispatch, the first thing React does is create an internal update record.
That record usually includes two kinds of information:
- What the new state should become
- How important or urgent this update is
Then React places that update into the update queue for the current component, where it waits to be processed later.
So it is better to think of setState not as "run the update now," but as "submit an update request."
3. Why do multiple setState calls not always cause multiple renders?
Because React has a very important optimization: batching.
If multiple state updates happen during the same event, React often will not re-render after each one. Instead, it collects them first and processes them together.
The benefit is straightforward: less repeated work, fewer unnecessary commits.
Here is a minimal example:
import { useState } from "react";
export default function Demo() {
const [count, setCount] = useState(0);
console.log("render", count);
const handleClick = () => {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
};
return (
<div>
<p>count: {count}</p>
<button onClick={handleClick}>+3</button>
</div>
);
}
After clicking the button, the typical result is:
countgoes from0to3- But React does not need three separate UI commits to get there
What happens here is not "each setCount immediately refreshes the page."
What happens is that React places the updates into a queue and computes the final result together.
That is why setState should never be treated as a simple synchronous value assignment.
4. React does more than collect updates - it also decides how urgent they are
Not every update matters equally.
For example:
- Typing into an input usually needs a fast response
- Refreshing a list, updating stats, or applying background data may not need to happen right away
So after React receives an update, it also considers questions like these:
- Does this need to run immediately?
- Can this wait a little?
- Should this be interrupted if something more important comes in?
That is one reason React can balance performance with responsiveness in larger apps.
You can think of React as a scheduler, not just an executor. It does not simply ask "what should I do?" It also asks:
Should I do this now, or is there a better time?
5. The moment React enters real computation
Once React decides it is time to process the update, it moves into the computation phase.
This phase is usually called the Render phase, but "render" here does not mean "the browser has already painted the new screen."
A better way to describe it is:
React is calculating what the next version of the UI should be.
At this point, the real DOM still has not changed.
6. What is React actually doing during the Render phase?
React starts from the current Fiber tree and walks it from the root, building a new working tree often called the WorkInProgress tree.
A useful way to picture it:
- The old tree represents the current UI
- The new tree is the next draft of the UI that React is building in memory
During this phase, three things matter most.
1) React applies queued updates and computes the latest state
The updates that were sitting in the queue do not stay there forever. During the Render phase, React consumes them and computes the latest state that this render should use.
So setState is responsible for submitting an update, but the actual state resolution happens during Render.
2) React checks what can be skipped entirely
React does not blindly recalculate the whole tree every time.
If a node meets certain conditions, for example:
- Its props did not change
- Its context did not change
- It has no pending work of its own
then React may reuse the old result and skip that subtree.
This is the foundation behind many React performance optimizations.
For example, React.memo is not some kind of magic trick.
What it really does is help React say:
If the inputs are the same, I do not need to re-run this component.
Here is a runnable example:
import { useState, memo } from "react";
const Child = memo(function Child({ label }) {
console.log("Child render");
return <div>Child: {label}</div>;
});
export default function Demo() {
const [count, setCount] = useState(0);
console.log("Parent render");
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<Child label="stable prop" />
</div>
);
}
In this example:
- The parent re-runs when you click the button
- The child can be skipped because its props stay the same and it is wrapped in
memo
That is React cutting out work that does not need to happen.
3) If something must update, React re-runs the component and compares the result
If a node cannot be skipped, React continues by:
- Executing the component again to get new JSX
- Comparing that new structure against the old Fiber structure
This comparison is what people usually refer to as reconciliation or diffing.
But there is an important detail here:
React still does not touch the DOM during this step.
Instead, it records what needs to happen later. That may include things like:
- This node should be inserted
- This node should be deleted
- This node's props or text should be updated
- This node has an effect that needs to be handled later
These instructions are stored as flags on Fiber nodes.
So the output of the Render phase is not "the page has changed." The output is:
React now knows exactly what needs to change.
7. Why can the Render phase be interrupted?
Because it is doing computation, not applying visible changes yet.
And if something is only computation, React has more flexibility. In some modes it can:
- Pause
- Resume
- Be interrupted by more urgent work
That is one of the key ideas behind concurrent rendering.
For example, imagine React is in the middle of a heavy low-priority update, and then the user types into an input. React does not have to stubbornly finish the older task first. It can prioritize the more important interaction.
The important idea here is:
Computation can be flexible. Committing visible changes must be consistent.
That leads directly to the next phase.
8. The only phase that actually changes the page is Commit
Once all the computation is done, React enters the Commit phase.
This is the phase where the page is actually affected.
You can think of everything before this as planning and preparation. Commit is where the plan is finally executed.
The simplest way to remember it is:
Render calculates. Commit applies.
9. The Commit phase usually has three parts
1) Before Mutation: preparation before the DOM changes
Before React mutates the DOM, it may first process information that has to be read while the old DOM is still intact.
A classic example is getSnapshotBeforeUpdate in class components. Its purpose is usually something like this:
- Capture information before the DOM changes
- Such as a scroll position or layout-related state
- Then use that information after the update
So this stage is not about changing the page yet. It is about gathering data that must be captured before the old DOM disappears.
2) Mutation: this is where real DOM work happens
This is the core of Commit.
React looks at the flags collected during Render and performs the actual DOM operations, such as:
- Inserting nodes
- Updating text
- Updating attributes
- Removing nodes
Only at this point does the user-facing page really change.
React also performs one very important internal step here: it switches the root pointer so the new Fiber tree becomes the current tree.
You can think of that as:
- The old tree represented the current UI
- The new tree now officially takes over
That is one reason React can separate "prepare the next version" from "activate the next version."
3) Layout: the DOM has changed, but the browser may not have painted yet
After the DOM is updated, the browser may still not have painted the result to the screen.
At this point, React runs synchronous layout-related logic, and the best-known example is useLayoutEffect.
This is useful for things like:
- Reading the latest DOM measurements
- Making immediate layout adjustments
- Preventing the user from seeing an incorrect intermediate state
Because this happens before paint, it runs synchronously. And because it is synchronous, heavy work here can delay what the user sees.
10. What is the real difference between useLayoutEffect and useEffect?
A lot of people memorize the conclusion like this:
useLayoutEffectruns earlieruseEffectruns later
That is not wrong, but it is also not the most useful explanation.
The deeper difference is that they are meant for different kinds of problems.
useLayoutEffect is for work that must finish before the screen is painted
For example, when you need to:
- Measure an element
- Synchronously adjust layout based on that measurement
- Avoid visible flicker
In those cases, useLayoutEffect is the better fit.
Here is a simple example:
import { useLayoutEffect, useRef, useState } from "react";
export default function Demo() {
const boxRef = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
if (boxRef.current) {
setWidth(boxRef.current.offsetWidth);
}
}, []);
return (
<div>
<div
ref={boxRef}
style={{ width: "240px", padding: "20px", border: "1px solid #333" }}
>
Hello React
</div>
<p>measured width: {width}px</p>
</div>
);
}
In this example, reading the DOM size belongs to the category of work that should happen before the UI is painted in its final form.
useEffect is for work that can happen after the screen is already shown
Typical examples include:
- Fetching data
- Subscribing to events
- Logging
- Syncing non-layout side effects
These do not usually need to block the initial display, so they belong in useEffect.
import { useEffect, useState } from "react";
export default function Demo() {
const [data, setData] = useState(null);
useEffect(() => {
const timer = setTimeout(() => {
setData("loaded");
}, 1000);
return () => clearTimeout(timer);
}, []);
return <div>{data ?? "loading..."}</div>;
}
Here, the page shows loading... first, and the side effect runs afterward.
That matches the design goal of useEffect.
11. Why is useEffect a bad fit for fixing layout flicker?
Because it usually does not block paint.
That means if your logic works like this:
- The page first appears in the wrong layout
- Then an effect fixes it afterward
the user may actually see that jump or flicker.
A practical rule of thumb is:
- Layout measurement or synchronous layout correction -> prefer
useLayoutEffect - Requests, subscriptions, and async side effects -> prefer
useEffect
Not all effects are interchangeable. That distinction matters a lot in real-world code.
12. Put the whole chain together
At this point, the full React update pipeline looks like this:
Step 1: an update is triggered
You call setState or dispatch, and React receives an update request.
Step 2: the update goes into a queue
React does not mutate the DOM immediately. It stores the update and tracks its priority.
Step 3: scheduling
React decides whether the update is urgent and when to process it.
Step 4: the Render phase
React computes the next version of the UI by:
- Consuming queued updates
- Re-running components that need work
- Skipping parts that do not
- Comparing old and new structures
- Recording the changes that will need to happen
Step 5: the Commit phase
React applies those calculated changes to the real DOM.
Step 6: layout-related logic
After the DOM is updated but before the browser paints, React runs useLayoutEffect and similar synchronous layout work.
Step 7: browser paint
The browser shows the updated UI.
Step 8: passive effects
After the browser gets a chance to paint, React runs passive effects such as useEffect.
13. What does all of this actually mean for developers?
Knowing the terminology is not enough. The real value is how this model changes the way you write and reason about code.
1) Do not treat setState as "the new DOM is available on the next line"
setState starts an update pipeline. It does not synchronously commit the new UI.
So in many cases, the next line of code does not yet have access to the final DOM for that update.
That is why DOM reads and side effects need to happen at the correct point in the lifecycle.
2) Performance work is usually about removing unnecessary updates, not forcing React to update faster
Many performance issues do not happen because React is inherently slow. They happen because React is asked to do too much unnecessary work.
For example:
- The parent updates frequently and causes many children to re-run
- New objects and functions are created every render, so children cannot be skipped
- Values that could stay stable keep changing instead
A lot of React optimization comes down to helping React answer one question more easily:
Has this part actually changed?
If the answer is no, React can skip the work.
3) Effect timing directly affects user experience
Flicker, layout jumps, and delayed display often are not caused by React itself being slow. They are caused by side effects happening at the wrong time.
This is common in real projects, which is why effect timing cannot be reduced to "both run after render."
The real question is:
- What must finish before paint?
- What is perfectly fine to do after paint?
14. A few easy misconceptions
Misconception 1: setState immediately updates the page
No. It submits an update request. The actual computation and commit happen later.
Misconception 2: the Render phase already changes the DOM
No. The Render phase is mainly about computing the next UI and recording what needs to change.
Misconception 3: every component is fully recalculated on every update
No. React tries hard to skip reusable parts.
Misconception 4: useEffect and useLayoutEffect only differ in timing
That is incomplete. They are really meant for different categories of work: layout-sensitive synchronous work versus ordinary side effects.
15. One sentence that captures React's update model
At its core, React's update mechanism is not "state changes, so change the DOM."
It is this:
React organizes updates as work, calculates the next UI based on priority and reusability, and only then commits the final result to the real page.
That design is what allows React to:
- Batch updates
- Assign priorities
- Support interruptible rendering
- Skip unnecessary work
- Keep DOM commits consistent
- Run side effects at the right time
Once you really understand that pipeline, many React concepts that seem separate at first, like Fiber, batching, reconciliation, concurrency, useLayoutEffect, useEffect, and performance optimization, begin to fit together into one mental model.
16. Three small experiments worth trying yourself
Experiment 1: verify batching
Goal:
- Call
setStatemultiple times in a row - Observe the final render behavior and result
What to focus on:
- Updates are queued first, then processed together
Experiment 2: verify skipped component updates
Goal:
- Update a parent component
- Wrap a child with
React.memo - Observe whether the child actually re-runs
What to focus on:
- The real optimization is skipping unnecessary work
Experiment 3: compare useLayoutEffect and useEffect
Goal:
- Build a small example that measures DOM size and then adjusts layout
- Test it once with each effect
What to focus on:
- Whether you can visibly notice flicker or layout jumping
Related reading
Reviewed by
DevDepth Editor
Editor and frontend engineering writer
DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.
Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.
Last editorial review: 2026-03-16