Understanding useEffect vs. useLayoutEffect in React
Choosing the right effect hook is key to avoiding subtle bugs and UI jank in React applications. The fundamental difference between useEffect and useLayoutEffect is their timing relative to the browser paint cycle. Grasping this mental model will lead to smoother user interfaces.
useEffect (The Default): Runs AFTER Paint
This is the standard side-effect hook. Its execution is deferred until after the browser has rendered the screen.
Step-by-Step Flow:
- React commits DOM changes. (New state is in the DOM.)
- Browser paints. (User sees the new frame.)
- useEffect callbacks fire. (Your code runs.)
Impact:
- Any DOM reading or mutation inside
useEffecthappens on the next visual frame. - Risk of Flicker: The user might momentarily see the old layout before your effect runs and potentially causes a layout jump or "flash of wrong content."
- Stale Measurements: If you read DOM properties like
offsetWidthorscrollTop, you might get values from the previous frame.
When to Use useEffect (The Default):
- Non-DOM-related side effects: Data fetching, logging, setting up subscriptions, timers, or analytics calls.
- The vast majority of side effects.
- When there is no visible flicker or layout issue.
Example Use:
useEffect(() => {
// Doesn't need to block the paint cycle
fetchUser(id).then(setUser);
}, [id]);
useLayoutEffect: Runs BEFORE Paint (Synchronous)
This hook is for interacting with the DOM immediately after React has made its changes, but before the browser paints the new frame.
Step-by-Step Flow:
- React commits DOM changes. (New state is in the DOM.)
- useLayoutEffect callbacks fire. (Synchronous, blocks paint)
- Browser does layout and paint. (User sees the final frame.)
Impact:
- Perfect for when you must read or mutate the DOM and ensure the result is visible before the user sees the screen.
- No Flicker: Ensures visual correctness by preventing layout shifts or stale measurements from being displayed.
The Trade-Off: Potential Jank
Because useLayoutEffect runs synchronously and blocks the main thread before the browser can paint, a long or slow operation inside it will delay the frame, potentially causing jank. It is a trade-off between visual correctness and performance risk.
When to Use useLayoutEffect (Only When Necessary)
Only use this hook when a DOM measurement or mutation is required to prevent a visual bug.
Good Reasons:
- Scroll Position Fixes: Setting
scrollTopor usingscrollIntoViewimmediately after content changes (e.g., in a chat window). - Dynamic Positioning: Measuring an element's size/position to correctly place another element (e.g., tooltips, popovers, arrows).
- Syncing DOM Elements: Calculating dimensions for sticky headers, fake scroll shadows, or other synchronization tasks.
- Preventing Layout Shift: Guaranteeing the layout is correct before the first paint.
Bad Reasons (Stick to useEffect):
- "I want it to run earlier" or "Just to be safe."
- Data fetching.
- Console logging.
Example Use:
useLayoutEffect(() => {
if (!ref.current) return;
// This mutation happens before paint, preventing a flash of the wrong scroll position
ref.current.scrollTop = ref.current.scrollHeight;
}, [messages]);
Avert Performance Pitfalls
Avoid Layout Thrashing: Multiple useLayoutEffect calls in a component tree can force numerous synchronous layout recalculations (reflows), leading to a massive performance hit. Batch your measurements when possible.
The Golden Rule: If you do not see a flicker, wrong initial position, or stale measurements with useEffect, then do not use useLayoutEffect. It is inherently slower and riskier for performance.
Quick Decision Tree
- Does it read or write DOM layout before the user sees it? Yes -> useLayoutEffect
- Does it fetch data, log, subscribe, or anything non-DOM-layout? Yes -> useEffect
- Is there no visual flicker/jump with the default hook? Yes -> useEffect (Default)
- Can CSS handle the layout/animation instead? Yes -> Delete the effect entirely
Bonus Ninja Tip: Prioritize CSS solutions (Flexbox, Grid, position: sticky, native transitions) over JavaScript for layout and styling to minimize effect timing problems.
Master the timing model, respect the paint cycle, and write butter-smooth React UIs. 🥷