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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.