Deep Dive into React useEffect and useLayoutEffect: Data Structures, Lifecycle, and Implementation Details

This article explains how React manages side‑effects with useEffect and useLayoutEffect, covering the underlying Effect data structure, the mount and update phases, the commit pipeline (before‑mutation, mutation, layout), and the differences in timing and execution between the two hooks.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Deep Dive into React useEffect and useLayoutEffect: Data Structures, Lifecycle, and Implementation Details

React builds user interfaces following functional programming principles, but real‑world components often need side‑effects such as data fetching, event subscription, or manual DOM manipulation. To handle these, React provides the useEffect and useLayoutEffect hooks, which manage side‑effects through a dedicated Effect data structure.

Effect Data Structure

Each Effect object contains the following properties:

tag : identifies the type of effect (e.g., useEffect or useLayoutEffect).

create : the callback passed as the first argument to the hook.

destroy : the cleanup function returned by create, executed when the effect is removed.

deps : the dependency array supplied to the hook.

next : a pointer that links Effects into a circular linked list.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">const effect: Effect = {
  tag,
  create,
  destroy,
  deps,
  // Circular
  next: (null: any),
};
</code>

When a component renders, React creates a memoizedState linked list of hooks on the fiber. Each hook entry points to its corresponding Effect, forming a circular Effect list.

Example Component

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">import React, { useEffect, useState, useLayoutEffect } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(1);
  };

  useEffect(() => {
    console.log(1);
  }, []);

  useLayoutEffect(() => {
    console.log(3);
  }, [3]);

  useEffect(() => {
    console.log(2);
  }, [count]);

  return <div>1</div>;
};

export default App;
</code>

During the mount phase, React builds the Effect list and stores it both on fiber.memoizedState and on fiber.updateQueue. The following diagram (originally an image) illustrates the resulting linked list.

Process Overview

React splits the handling of Effects into two main phases: the render phase and the commit phase. The render phase creates the hook and Effect linked lists, while the commit phase actually runs the side‑effect callbacks.

Render Phase

During rendering, React creates a hook list on the work‑in‑progress fiber ( memoizedState) and simultaneously builds an Effect list. The type of Effect (passive or layout) determines whether it will be processed in the useEffect or useLayoutEffect path.

Mount Phase Functions

The mount path calls mountEffect, which forwards to mountEffectImpl:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">function mountEffect(create, deps) {
  if (__DEV__ && enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode) {
    return mountEffectImpl(MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);
  } else {
    return mountEffectImpl(PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);
  }
}
</code>
mountEffectImpl

creates a new Hook, normalises the dependency array, sets the appropriate fiber flags, and pushes the Effect into the circular list via pushEffect:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}
</code>

The pushEffect function builds the Effect object and inserts it into the component’s update queue, creating a circular linked list if necessary:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">function pushEffect(tag, create, destroy, deps) {
  const effect = { tag, create, destroy, deps, next: (null: any) };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
</code>

Update Phase

When a component updates (e.g., state changes), React calls updateEffect, which forwards to updateEffectImpl. This function retrieves the previous Effect, compares dependency arrays, reuses the existing Effect if dependencies are unchanged, or creates a new one otherwise.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);
}
</code>

Commit Phase

After the render phase finishes, React enters the commit phase, which consists of three sub‑phases: before‑mutation, mutation, and layout. Each sub‑phase walks the Effect list and performs specific actions.

Before‑Mutation

In commitBeforeMutationEffects, React processes any cleanup work that must happen before the DOM is mutated (e.g., removing refs). The core loop is:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">while (nextEffect !== null) {
  const fiber = nextEffect;
  // ... handle deletions, traverse children ...
}
</code>

Mutation

During the mutation sub‑phase, React applies DOM changes. Functions such as commitMutationEffectsOnFiber handle deletions, insertions, and property updates. For example, commitPlacement inserts newly created DOM nodes, while commitUpdate updates existing node properties.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">export function commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  updateFiberProps(domElement, newProps);
}
</code>

Layout

The layout sub‑phase runs synchronously after the DOM has been mutated but before the browser paints. It executes layout Effects ( useLayoutEffect) and class‑component lifecycle methods such as componentDidMount. The main loop is:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">while (nextEffect !== null) {
  const fiber = nextEffect;
  const firstChild = fiber.child;
  if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
    firstChild.return = fiber;
    nextEffect = firstChild;
  } else {
    commitLayoutMountEffects_complete(...);
  }
}
</code>

For function components, commitHookEffectListMount runs the create callbacks of layout Effects and stores any returned cleanup function in effect.destroy:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Mentrone, monospace; font-size: 12px;">function commitHookEffectListMount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
</code>

useEffect vs. useLayoutEffect

useEffect

creates passive Effects that are scheduled by the React Scheduler and run asynchronously after the browser’s idle time, ensuring they do not block rendering. useLayoutEffect creates layout Effects that run synchronously during the layout sub‑phase, before the browser paints, and can therefore block rendering if they are expensive.

Conclusion

Both hooks share the same underlying Effect data structure and are processed through the same linked‑list mechanism. The key differences lie in when they are executed: useEffect runs after the commit phase (asynchronously), while useLayoutEffect runs during the layout sub‑phase (synchronously). Understanding this pipeline helps developers write performant side‑effect code and avoid unnecessary re‑renders.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScripthooksuseEffectuseLayoutEffect
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.