React Internals: Which useEffect runs first?
React Internals: Which useEffect runs first? êŽë š
useEffect
is one of the most commonly used hooks in the React community. Regardless of how much experience you have with React, youâve probably used it before.
But have you ever run into situations where useEffect
hooks run in an unexpected order when multiple layers of components are involved?
Letâs start with a quick quiz. Whatâs the correct order of these console.log
statements in the console?
function Parent({ children }) {
console.log("Parent is rendered");
useEffect(() => {
console.log("Parent committed effect");
}, []);
return <div>{children}</div>;
}
function Child() {
console.log("Child is rendered");
useEffect(() => {
console.log("Child committed effect");
}, []);
return <p>Child</p>;
}
export default function App() {
return (
<Parent>
<Child />
</Parent>
);
}
Answer
// initial render
Parent is rendered
Child is rendered
// useEffects
Child committed effect
Parent committed effect
If you got it rightânice job! If not, no worriesâmost React devs get this wrong too. In fact, this isnât something thatâs clearly documented or explained on the official React website.
Letâs explore why children components are rendered last but their effects are committed first. Weâll dive into how and when React renders components and commits effects (useEffect
). Weâll touch on a few React internal concepts like the React Fiber architecture and its traversal algorithm.
Overview of React Internals
According to React official documentation, the entire Reactâs component lifecycle can be roughly divided into 3 phases: Trigger â Render â Commit
Triggering a render
- The componentâs initial render, or state updates with
setState
. - A state update is put in a queue and scheduled to be processed by the React Scheduler.
Rendering
- React calls the component and works on the state update.
- React reconciles and marks it as âdirtyâ for commit phase.
- Create new DOM node internally.
Committing to the DOM
- Apply actual DOM manipulation.
- Runs effects (
useEffect
,useLayoutEffect
).
React Fiber Tree
Before diving into the traversal algorithm, we need to understand the React Fiber architecture. Iâll try to keep this introduction beginner-friendly.
Internally, React uses a tree-like data structure called fiber tree to represent the component hierarchy and track updates.

From the diagram above, we can tell that fiber tree is not exactly a one-to-one mapping of DOM tree. It includes additional information that help React manage rendering more efficiently.
Each node in this tree is called a fiber node. There are different kinds of fiber nodes such as HostComponent
which refers to a native DOM element, like <div>
or <p>
in the diagram. FiberRootNode
is the root node and will point to a different HostRoot
node during each new render.
Every fiber node contains properties like props
, state
, and most importantly:
child
â The child of the fiber.sibling
â The sibling of the fiber.return
â The return value of the fiber is the parent fiber.
These information allows React to form a tree.
Every time there is a state update, React will construct a new fiber tree and compare against the old tree internally.
Note
If youâre interested in the detail, please check out JSerâs blog or his super cool project React Internal Explorer!
How Fiber Tree Is Traversed
Generally, React reuses the same traversal algorithm in many use cases.

The animation above shows how React walks the fiber tree. Notice that each node is stepped twice. The rule is simple:
- Traverse downwards.
- In each fiber node, React checks
- If thereâs a child, move to the child.
- If thereâs no child, step again the current node. Then,
- If thereâs a sibling, move to the sibling.
- If thereâs no sibling, move up to its parent.
This traversal algorithm ensures each node is stepped twice.
Now, letâs revisit the quiz above.
Render Phase

React traverses the fiber tree and recursively performs two steps on each fiber node:
- In the first step, React calls the component â this is where
console.log
statement is executed. React reconciles and marks the fiber as âdirtyâ if state or props have changed, preparing it for the commit phase. - In the second step, React constructs the new DOM node.
Info
In the React source code, the process is named workLoop
(facebook/react
). The first step is beginWork()
(facebook/react
). The second step is completeWork()
(facebook/react
).
At the end of Render phase, a new fiber tree with the updated DOM nodes is generated. At this point, nothing has been committed to the real DOM yet. The actual DOM mutations will happen in the Commit phase.
Commit Phase
Commit phase is where actual DOM mutations and effect flushing (useEffect
). The traversal pattern remains the same, but DOM mutations and effect flushing are handled in separate walks.
In this section, weâll skip DOM mutations and focus on the effect flushing walk.
Committing effects
React uses the same traversal algorithm. However, instead of checking whether a node has a child, it checks whether it has a subtree â which makes sense, because only React components can contain useEffect
hooks. A DOM node like <p>
wonât contain any React hooks.
Nothing happens in the first step, but in the second step, it commits effects.

This depth-first traversal explains why child effects are run before parent effects. This is the root cause.
Note
In the React source code, the recursive function for committing effects is named recursivelyTraversePassiveMountEffect
(facebook/react
).
Now letâs check out another quiz example. The result should make more sense to you now.
function Parent({ children }) {
console.log("Parent is rendered");
useEffect(() => {
console.log("Parent committed effect");
}, []);
return <div>{children}</div>;
}
function Child() {
console.log("Child is rendered");
useEffect(() => {
console.log("Child committed effect");
}, []);
return <p>Child</p>;
}
function ParentSibling() {
console.log("ParentSibling is rendered");
useEffect(() => {
console.log("ParentSibling committed effect");
}, []);
return <p>Parent's Sibling</p>;
}
export default function App() {
return (
<>
<Parent>
<Child />
</Parent>
<ParentSibling />
</>
);
}
Answer
// Initial render
Parent is rendered
Child is rendered
ParentSibling is rendered
// useEffects
Child committed effect
Parent committed effect
ParentSibling committed effect
During the commit phase:

Now, it should be self-explanatory why child effects are flushed before their parents during the commit phase.
Understanding how and when React commits useEffect
hooks can help you avoid subtle bugs and unexpected behaviorsâespecially when working with complex component structures.
Welcome to React internals!