In-Depth Article
useEffect vs useLayoutEffect: The Real Difference in React’s Commit Phase
Understand how React Fiber stores both hooks in the same effect list, why HookLayout and HookPassive tags matter, and when each runs during the commit phase.
If you really want to understand the difference between useEffect and useLayoutEffect, it is not enough to stop at “one runs earlier and one runs later.” A more accurate way to look at it is through React’s Fiber architecture, Effect tags, and the execution flow of the Commit phase.
Here is the core takeaway:
At the data structure level, useEffect and useLayoutEffect are almost identical. They are both stored in the same Effect list. The real difference is not where they are stored, but how they are tagged, and when React processes them during the Commit phase.
1. At the data structure level, they live in the same Effect list
Inside React, both useEffect and useLayoutEffect eventually create an Effect object. These objects are basically the same in structure, and they are not stored separately.
During the Render phase, React maintains an updateQueue on the current Fiber node. Inside that queue, there is an important pointer called lastEffect. All Effects are linked together in declaration order, forming a singly linked circular list.
A simplified version looks like this:
const effect = {
tag: HookLayout | HookPassive, // The key difference: different tags
create: () => { ... }, // The effect callback
destroy: undefined, // Cleanup function
deps: [count], // Dependency array
next: null, // Points to the next effect, eventually forming a ring
};
The most important field here is tag:
Effects created by useLayoutEffect carry HookLayout
Effects created by useEffect carry HookPassive
That means this:
In memory, useEffect and useLayoutEffect are actually mixed together in the same list. React does not separate them into two different lists. It tells them apart by checking their bit flags.
So when React later walks through this list, it uses bitwise checks to figure out what kind of Effect each one is, and then decides when it should run.
2. The real difference is how they are executed during the Commit phase
During the Render phase, React figures out what needs to change. During the Commit phase, it actually applies those changes.
If we only focus on how Effects are handled, the Commit phase can be broken down into a few important parts.
3. Mutation Phase: update the DOM first
The first step of Commit is the Mutation Phase.
At this stage, React applies changes to the real DOM. In other words, by this point, the DOM nodes have already been updated.
But there is one thing to keep in mind:
Even though the DOM has changed, the browser may not have painted those changes to the screen yet.
That is exactly why useLayoutEffect and useEffect behave differently.
4. Layout Phase: useLayoutEffect runs here synchronously
useLayoutEffect runs after the DOM has been updated, but before the browser paints.
Its characteristics are:
The DOM is already updated
The browser has not painted yet
React runs Layout Effects synchronously
At this stage, React traverses the relevant Fiber nodes, then walks through the Effect circular list on each Fiber, and filters out Effects tagged with HookLayout.
The execution flow usually looks like this:
Run the previous destroy
Immediately run the current create
That is why useLayoutEffect is often described like this:
It runs synchronously after DOM updates, but before the browser paints.
This makes it useful for tasks that are tightly related to layout, such as reading DOM measurements, synchronously adjusting styles, or preventing visual flicker.
But because it runs synchronously, expensive work here can delay painting. And if you accidentally create an infinite loop, the browser will be blocked from rendering altogether.
5. Passive Phase: useEffect is deferred
useEffect does not run synchronously during the Layout phase.
Before Commit fully finishes, React schedules Passive Effects to be processed later. Then it uses flushPassiveEffects to actually run them.
The overall flow looks like this:
React finishes updating the DOM
React synchronously runs useLayoutEffect
React schedules useEffect for later
The browser gets a chance to paint
After that, React runs flushPassiveEffects
It walks through the same Effect list and picks out Effects tagged with HookPassive
It runs the old destroy, then the new create
So from the outside, useEffect is usually understood as:
It runs after the page has been painted, through deferred scheduling.
That is why most side effects, such as fetching data, subscribing to events, or logging, belong in useEffect rather than useLayoutEffect.
6. At the bottom, the difference comes down to just two things
If you strip everything down, the underlying difference between useEffect and useLayoutEffect comes down to only two points.
- Different tags
Even though both are stored in the same Effect list, their tag values are different:
useLayoutEffect uses HookLayout
useEffect uses HookPassive
React uses these tags to tell them apart.
- Different timing when React consumes the list
React processes side effects more than once during Commit, but not in the same way:
useLayoutEffect: runs synchronously during the Layout phase
useEffect: runs later during the Passive phase through deferred scheduling
So the real story is not that they use different data structures. It is this:
The same Effect list is consumed at different times, under different rules.
7. A simple example
Suppose a component uses both Hooks:
function App() {
useEffect(() => {
console.log("A");
});
useLayoutEffect(() => {
console.log("B");
});
}
From React’s internal point of view, these two Hooks might end up in a circular list like this:
Fiber.updateQueue.lastEffect
-> Effect(A, tag: Passive)
-> Effect(B, tag: Layout)
-> back to A
Notice that they are stored in the same list. The only difference is the tag.
When the Commit phase runs, the logic is roughly like this:
function commitRoot(root) {
// 1. Update the DOM
commitMutationEffects(root);
// 2. Run Layout Effects synchronously
commitLayoutEffects(root);
// Traverse the effect list, pick HookLayout
// Hit B, run B
// 3. Schedule Passive Effects
scheduleCallback(() => {
flushPassiveEffects();
// Traverse the same list again, pick HookPassive
// Hit A, run A
});
}
So the usual output is:
B A
That is why useLayoutEffect runs before useEffect.
8. Why React provides both
React provides both useEffect and useLayoutEffect because they solve two different kinds of side effects.
useLayoutEffect is a better fit when:
you need to read layout information before paint
you need to change the DOM immediately before paint
you want to avoid visible flicker
useEffect is a better fit when:
fetching data
subscribing and unsubscribing to events
logging
most side effects that do not need to happen before paint
From a practical perspective, you can think of it like this:
useLayoutEffect is more about controlling what the user sees during render, while useEffect is more about handling work after render is done.
9. Summary
The underlying difference between useEffect and useLayoutEffect is not that they use completely different data structures. The real differences are:
they are attached to the same circular Effect list
their tags are different
React processes them at different times during the Commit phase
You can sum it up like this:
At the storage level, useLayoutEffect and useEffect are almost the same. What makes them behave differently is the Effect tag and the timing of execution during Commit.
Or even more simply:
useLayoutEffect: synchronous, can block paint, good for layout-related work
useEffect: deferred, does not block paint, good for most side effects
That is also why, in everyday development, useEffect should usually be your default choice. Reach for useLayoutEffect only when you truly need code to run immediately after DOM updates but before the browser paints.
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