Frontend Development 28 min read

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.

const effect: Effect = {
  tag,
  create,
  destroy,
  deps,
  // Circular
  next: (null: any),
};

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

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
1
;
};

export default App;

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 :

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);
  }
}

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

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);
}

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

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;
}

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.

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);
}

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:

while (nextEffect !== null) {
  const fiber = nextEffect;
  // ... handle deletions, traverse children ...
}

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.

export function commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  updateFiberProps(domElement, newProps);
}

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:

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(...);
  }
}

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

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);
  }
}

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.

frontendJavaScriptReactHooksuseEffectuseLayoutEffect
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

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