Why is useEffect not guaranteed to run right after commit?
1. What the commit phase does
In React, every update cycle is split into two big parts:
| Phase | What it does | Can it be interrupted? |
|---|---|---|
| Render Phase | React computes what needs to change (a new Virtual DOM) | Yes |
| Commit Phase | React applies the changes to the real DOM | No |
After the commit phase, the DOM is already updated, the screen is repainted, and React is ready to call effects (useEffect, useLayoutEffect, componentDidMount, and so on).
But, importantly, not all effects have the same priority.
2. Different types of effects
React distinguishes two types of side effects:
| Type | Hook | When it runs |
|---|---|---|
| Synchronous (layout) | useLayoutEffect | right after commit, before the screen is painted |
| Passive | useEffect | asynchronously, after painting and the browser's paint tick |
3. Why useEffect() is asynchronous
After commit, React:
- Updates the DOM.
- Tells the browser: "ready, the screen can be repainted".
- Waits for the browser to finish painting.
- Only then calls all the "passive" effects (
useEffect).
This is needed so that:
- the UI becomes visible as fast as possible;
- effects (network requests, timers, subscriptions, and so on) do not slow down the repaint;
- the browser stays responsive (scroll, input, animation are not blocked).
Example (the difference between useLayoutEffect and useEffect)
useLayoutEffect(() => {
console.log('useLayoutEffect'); // synchronous, after the DOM update
});
useEffect(() => {
console.log('useEffect'); // asynchronous, after paint
});Console log:
render
commit
useLayoutEffect ← right after commit
PAINT (browser)
useEffect ← after painting4. Why this matters in React 18 and concurrent rendering
In React 18, rendering became interruptible and priority-based (Concurrent Rendering). Because of this, React can:
- delay the
useEffect()call if there are more urgent tasks (for example, the user clicked something); - cancel effects if the render was aborted before commit;
- merge several renders into one commit and call the effects only once after it.
So React guarantees only one thing:
useEffect() will be called after the DOM has already been updated and the browser has shown the changes,
but not necessarily instantly - it could be on the next event loop tick.
5. What if you need an effect "right after commit"?
Then you need to use useLayoutEffect():
useLayoutEffect(() => {
console.log('Synchronously after commit');
});useLayoutEffect:
- runs synchronously, right after React updates the DOM;
- blocks painting until the callback finishes;
- is ideal for measurements (for example,
getBoundingClientRect).
But using
useLayoutEffectwithout a real need is risky, it can delay the UI from rendering. For most side effects (fetch, subscriptions, timers),useEffectis preferred.
6. Visually
Render phase
↓
Commit phase (DOM updated)
↓
→ useLayoutEffect (right away)
↓
Browser painted
↓
→ useEffect (asynchronously)7. Summary
React does not call
useEffectright after commit, becauseuseEffectis a passive effect that runs asynchronously after the browser has finished painting.This guarantees that:
- the UI renders as fast as possible,
- effects do not block the interface,
- concurrent rendering stays responsive.
| Hook | When it runs | Use it for |
|---|---|---|
useLayoutEffect | right after commit (synchronously) | DOM measurements, layout synchronization |
useEffect | after paint (asynchronously) | side effects, requests, subscriptions |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.