Skip to content
DevDepth
← Back to all articles

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.

Published: Updated: 6 min readreact-internals

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.

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

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

Contact the editor