component rendering order and hook calling in React
Component rendering order in React - React traverses the component tree depth-first on every update, calling each component function top-down, while hooks inside each component must fire in the exact same sequence every single render.
Theory
TL;DR
- Rendering is like a family dinner: parent sits first, then calls each child to the table one by one before moving to the next guest at the table
- React renders parent, walks into children depth-first, then moves to siblings
- Hooks are identified by call position in a per-component linked list, not by name
- If hook count changes between renders, React throws an error
- Condition logic belongs inside hooks, never around them
Quick example
function Parent() {
console.log('Parent render'); // 1st
return <Child />;
}
function Child() {
console.log('Child render'); // 2nd
const [count, setCount] = useState(0); // Hook 1 - always position 0
const ref = useRef(null); // Hook 2 - always position 1
useEffect(() => {
console.log('Child effect'); // 4th - after browser paint
}, []);
console.log('Child hooks done'); // 3rd
return <div ref={ref}>{count}</div>;
}
// Console output:
// Parent render
// Child render
// Child hooks done
// Child effectParent renders, then Child renders synchronously, then effects fire after the browser paints. That sequence is fixed.
Rendering order: depth-first traversal
React walks the JSX tree top-down. Given this structure:
function App() {
return (
<>
<SiblingA />
<Parent />
<SiblingB />
</>
);
}
function Parent() { return <><ChildA /><ChildB /></>; }
function ChildA() { console.log('ChildA'); return null; }
function ChildB() { console.log('ChildB'); return null; }
function SiblingA() { console.log('SiblingA'); return null; }
function SiblingB() { console.log('SiblingB'); return null; }
// Output: App -> SiblingA -> Parent -> ChildA -> ChildB -> SiblingBReact goes deep into each subtree before moving to the next sibling. SiblingB does not start rendering until the entire Parent subtree is done.
This matters when debugging. If ChildA is slow, it blocks ChildB and SiblingB. Adding console.log at the top of each component function shows exactly this traversal. In practice, this single technique catches more unexpected re-render bugs than most profiling sessions.
How React tracks hook state
Every component in the Fiber architecture has its own linked list of hook states. Three hooks in a component means React stores them at positions 0, 1, and 2 in that list.
On re-render, React replays those calls in the same order and reads position 0, 1, 2 to get the previous values. No names. No magic. Just index position.
This is why hook order cannot change. Skip hook 1 on one render, and hook 2 now sits at position 0. React reads the wrong state. The bug is often silent until something breaks.
Hook execution phases
Hooks split into two groups based on when they fire.
During render: useState, useReducer, useContext, useRef, useMemo, useCallback. These run synchronously as the component function executes, in declaration order.
After render: useLayoutEffect fires after the DOM is updated but before the browser paints. Use it when you need to measure a DOM element or fix layout before the user sees the screen. useEffect fires after the browser finishes painting. That is the right place for data fetching, subscriptions, and timers.
Cleanup runs in reverse order on unmount: children clean up before parents.
When to apply this knowledge
- Debugging re-renders: log component names at the top of each function, check which ones fire when they should not
- Custom hooks: always call them at the top level, never inside loops or conditions - a custom hook is a function that calls other hooks, the same rules apply
- Performance: wrap subtrees in
React.memoto stop parent re-renders from cascading into children that do not depend on the changed state useLayoutEffectvsuseEffect: if you see a visual flicker when updating element size or position, switch fromuseEffecttouseLayoutEffect- it prevents the browser from painting the intermediate state
Common mistakes
Conditional hook call:
// Wrong
function Bad({ show }) {
if (show) {
const [count, setCount] = useState(0); // skipped when show is false
}
return <div />;
}
// Right
function Good({ show }) {
const [count, setCount] = useState(0); // always called
return show ? <div>{count}</div> : null;
}When show flips from true to false, the hook at position 0 disappears. React reads the next hook's value into the wrong slot. You get wrong state or a crash with "Rendered fewer hooks than expected".
Hook inside a loop:
// Wrong
function BadList({ items }) {
items.forEach(item => {
const [state, setState] = useState(0); // count shifts with items.length
});
}
// Right
function GoodList({ items }) {
const [states, setStates] = useState(() => items.map(() => 0));
// one useState holds the whole array
}React throws "Rendered more hooks than during the previous render" when items.length changes. One useState holding an array solves it.
Early return before hooks:
// Wrong
function Bad({ user }) {
if (!user) return null; // hooks below never called on this render
const [active, setActive] = useState(false);
return <div>{active}</div>;
}
// Right
function Good({ user }) {
const [active, setActive] = useState(false); // always runs first
if (!user) return null;
return <div>{active}</div>;
}Condition inside a custom hook:
// Wrong
function useData(id) {
if (!id) return null;
const [data, setData] = useState(null); // conditional call inside hook
}
// Right
function useData(id) {
const [data, setData] = useState(null);
useEffect(() => {
if (id) fetchData(id).then(setData);
}, [id]);
return data;
}Real-world usage
- Next.js: server-side rendering and hydration depend on matching render order between server and client. A hook order mismatch causes hydration errors that are hard to trace
- TanStack Query:
useQuerymust be called at the top level - its cache key depends on stable call position - Redux Toolkit:
useSelectorin connected components follows the same linked list mechanism - React DevTools Profiler: shows render order and timing for every component in the tree
- Concurrent React 18: render phase stays depth-first and synchronous; the commit phase can yield for priority work, but component and hook order within a single render pass is identical to previous React versions
Follow-up questions
Q: What is the render order for <Parent><A/><B/></Parent><Sibling/>?
A: Parent -> A -> B -> Sibling. React finishes the entire Parent subtree before moving to Sibling.
Q: Why can't you call useState inside an if statement?
A: React identifies hooks by their call index in a per-fiber linked list. Skipping a call shifts all subsequent indices, so React reads wrong values on the next render.
Q: What is the cleanup order when components unmount?
A: Reverse of mounting. Children clean up before parents. This mirrors the bottom-up order in which effects are committed.
Q: How does useLayoutEffect differ from useEffect in timing?
A: useLayoutEffect fires synchronously after the DOM mutation but before browser paint. useEffect fires after paint. Use useLayoutEffect when you need to read or write DOM measurements before the user sees the result.
Q: In React 18 concurrent mode, can render order change?
A: The depth-first traversal order stays the same. Concurrent mode can pause and restart render work for priority, but within a single render pass the sequence is identical to previous React versions.
Q (senior): A component re-renders with a shorter list. Hooks are correctly declared outside the loop. But useCallback handlers still reference old item values. Why?
A: Stale closures. useCallback captures variables from the render when the callback was last created. If the dependency array does not include the changed values, the callback holds a reference to the old state. Add those values to the dependency array, or store the latest value in a ref to avoid triggering extra re-renders.
Examples
Basic: render order across a component tree
function App() {
console.log('App');
return (
<>
<Header />
<Main />
<Footer />
</>
);
}
function Main() {
console.log('Main');
return (
<>
<Sidebar />
<Content />
</>
);
}
function Header() { console.log('Header'); return <header />; }
function Sidebar() { console.log('Sidebar'); return <aside />; }
function Content() { console.log('Content'); return <main />; }
function Footer() { console.log('Footer'); return <footer />; }
// Output:
// App
// Header
// Main
// Sidebar
// Content
// FooterHeader completes before Main starts. Sidebar and Content both finish before Footer begins. Depth-first, siblings after all children of a parent.
Intermediate: hooks inside a real dashboard component
function UserDashboard({ userId }) {
// All hooks declared unconditionally at the top
const [user, setUser] = useState(null); // Hook 1: always position 0
const [stats, setStats] = useState({}); // Hook 2: always position 1
const containerRef = useRef(null); // Hook 3: always position 2
useEffect(() => {
if (userId > 0) {
fetchUser(userId).then(setUser); // condition goes inside, not around
}
}, [userId]);
if (!userId) return null; // early return AFTER all hooks
return (
<div ref={containerRef}>
<UserProfile user={user} />
<StatsPanel data={stats} />
</div>
);
}
// Render order: UserDashboard -> UserProfile -> StatsPanel
// Hook order per render: useState(null), useState({}), useRef, useEffectAll three hooks fire on every render regardless of userId. The condition moves inside useEffect. The early return sits after hooks are done. This is the pattern React expects.
Advanced: diagnosing and fixing a hook order bug
// Bug: hook count changes when items.length changes
function BadList({ items }) {
const renderedItems = items.map(item => {
const [selected, setSelected] = useState(false); // wrong: different count each render
return (
<li key={item.id} onClick={() => setSelected(s => !s)}>
{item.name} {selected ? '(selected)' : ''}
</li>
);
});
return <ul>{renderedItems}</ul>;
}
// React error: "Rendered more hooks than during the previous render."
// Fix: one useReducer at the top level, stable hook count
function GoodList({ items }) {
const [selected, dispatch] = useReducer(
(state, id) => ({ ...state, [id]: !state[id] }),
{}
);
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => dispatch(item.id)}>
{item.name} {selected[item.id] ? '(selected)' : ''}
</li>
))}
</ul>
);
}Moving per-item state into a loop breaks the fixed-length rule. One useReducer at the top level handles all items, keeps hook count stable at 1 regardless of list size, and avoids the crash when items are added or removed.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.