React 48 ⚛️ Synchronous DOM Reads with useLayoutEffect
useEffect is the hook that runs the side effects after the render is committed to the screen. The browser has already painted the DOM, and the effect runs asynchronously, after the paint. For most effects — the data fetching, the subscriptions, the logging — this is exactly right. The effect does not need to run before the paint, and running it after keeps the browser responsive. But some effects must run before the browser paints. They read the DOM, measure the element, and update the state based on the measurement. If they run after the paint, the user sees the wrong layout for a frame: the element is rendered in the wrong position, then the effect measures it, then the state updates, then the element moves. The flash is visible, and it is the bug that useLayoutEffect exists to fix.
The distinction between useEffect and useLayoutEffect is the timing. useEffect runs asynchronously, after the browser paints. useLayoutEffect runs synchronously, after the DOM is mutated but before the browser paints. The effect can read the DOM, measure the element, and trigger a synchronous re-render, and the user sees only the final result. The block is the whole point: the browser waits for the layout effect to finish before it paints, so the effect’s synchronous work is invisible. The cost is the blocking: the layout effect delays the paint, and a slow layout effect makes the page feel sluggish.
The rule of thumb is to use useEffect by default and to reach for useLayoutEffect only when the effect reads the DOM and updates the state based on the read. The measurement, the scroll position, the tooltip positioning, the animation initial state — these are the layout effects. The data fetching, the event subscriptions, the analytics — these are the regular effects. The useLayoutEffect in the server-side rendering produces the warning, because the server has no DOM to measure, and the warning is the reminder that the layout effect is the browser-only hook.
This chapter covers three areas. First, why useLayoutEffect exists — the pre-paint measurement problem and the flicker. Second, how the timing works — the order of the render, the DOM mutation, the layout effect, the paint, and the passive effect. Third, the patterns and the pitfalls — the measurement, the tooltip, the scroll, the SSR warning, and the cases where the layout effect hurts more than it helps. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the effect timing.
Key point: useLayoutEffect runs synchronously after the DOM is mutated but before the browser paints. It is for the effects that read the DOM and update the state based on the read, so the user never sees the intermediate state. useEffect runs asynchronously after the paint, and it is the default. The useLayoutEffect blocks the paint, and it produces a warning in the server-side rendering.
Why useLayoutEffect exists
The pre-paint measurement problem. The browser renders the DOM in two steps: the layout and the paint. The layout computes the position and the size of the elements, and the paint draws them to the screen. A component that needs to measure its size must read the layout, and the read is valid only after the layout. The useEffect runs after the paint, so the measurement happens after the user has seen the element. The useLayoutEffect runs after the layout but before the paint, so the measurement happens before the user sees the element.
The flicker problem. The classic example is the tooltip. The tooltip is rendered at the default position, the useEffect measures the trigger, the state updates, and the tooltip moves to the correct position. The user sees the tooltip at the wrong position for a frame, then at the correct position. The flicker is the visible bug. The useLayoutEffect measures the trigger and updates the state before the paint, so the tooltip is painted at the correct position the first time.
The scroll-position problem. A component that scrolls to the bottom on the mount must read the scroll height and set the scroll top. With the useEffect, the element is painted at the top, then the effect scrolls it to the bottom, and the user sees the jump. With the useLayoutEffect, the scroll happens before the paint, and the user sees only the bottom.
The animation-initial-state problem. An animation that starts from a computed position must set the initial state before the paint. The useLayoutEffect computes the position and applies the initial transform, and the animation starts from the correct place. The useEffect would let the element paint at the default position, then jump to the computed position.
The synchronous-re-render problem. The useLayoutEffect can trigger the synchronous re-render. When the effect calls the setState, React re-renders the component synchronously, before the browser paints. The user sees only the final render, and the intermediate render is invisible. The useEffect triggers the asynchronous re-render, and the intermediate render is painted.
The trade-off. The useLayoutEffect blocks the paint. The browser waits for the effect to finish before it draws the screen. A slow layout effect — a heavy measurement, a synchronous loop, the layout thrashing — makes the page feel sluggish, because the paint is delayed. The useEffect does not block the paint, and the page stays responsive. The trade-off is between the correctness of the pre-paint measurement and the responsiveness of the post-paint effect. The useLayoutEffect is the tool for the cases where the correctness matters.
a. The timing order
The order of the operations in the React commit phase is fixed. The useLayoutEffect and the useEffect are at the different points in the order.
1. The render phase
- React calls the component function.
- React computes the new element tree.
- React reconciles the tree with the previous one.
2. The DOM mutation
- React applies the changes to the DOM.
- The browser updates the DOM tree.
- The DOM is now the "committed" state.
3. The layout effects (useLayoutEffect)
- React runs the useLayoutEffect callbacks synchronously.
- The callbacks can read the DOM (the layout is computed).
- The callbacks can setState, which triggers a synchronous re-render.
- The re-render's effects run before the paint.
4. The browser paint
- The browser paints the DOM to the screen.
5. The passive effects (useEffect)
- React runs the useEffect callbacks asynchronously.
- The callbacks run after the paint.
- The callbacks can setState, which triggers an asynchronous re-render.
The key insight is the position of the two effects relative to the paint. The useLayoutEffect is before the paint, and the useEffect is after. The useLayoutEffect is the last chance to read the DOM and change the output before the user sees it.
The cleanup order. The cleanup functions run in the reverse order. When the component re-renders, the previous useLayoutEffect‘s cleanup runs before the new useLayoutEffect, and the previous useEffect‘s cleanup runs before the new useEffect. The layout cleanups run before the paint, and the passive cleanups run after.
The synchronous re-render. The useLayoutEffect that calls the setState triggers the synchronous re-render. React re-renders the component, applies the new DOM mutations, runs the layout effects again (including the new ones), and then paints. The user sees only the final state. The useEffect that calls the setState triggers the asynchronous re-render, and the intermediate state is painted.
b. The basic useLayoutEffect
The useLayoutEffect has the same signature as the useEffect: the callback and the dependency array.
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ targetRef, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const target = targetRef.current;
const tooltip = tooltipRef.current;
if (!target || !tooltip) return;
const targetRect = target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
setPosition({
top: targetRect.top - tooltipRect.height - 8,
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
});
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
>
{children}
</div>
);
}
The useLayoutEffect measures the target and the tooltip with the getBoundingClientRect, computes the position, and calls the setPosition. The call triggers the synchronous re-render, and the tooltip is painted at the correct position the first time. The useEffect would let the tooltip paint at the top: 0, left: 0 position, then move it, and the user would see the jump.
The dependency array. The useLayoutEffect takes the dependency array, and the effect re-runs when the dependencies change. The empty array runs the effect once, on the mount. The missing array runs the effect on every render, which is the source of the performance problems.
The cleanup. The useLayoutEffect can return the cleanup function, and the cleanup runs before the next effect and on the unmount.
useLayoutEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
The cleanup is the same as the useEffect, and the timing is the difference.
c. The patterns
The measurement pattern. The component measures the element and updates the state based on the measurement.
function AutoSizeBox({ children }) {
const [height, setHeight] = useState(0);
const boxRef = useRef(null);
useLayoutEffect(() => {
if (boxRef.current) {
setHeight(boxRef.current.getBoundingClientRect().height);
}
}, [children]);
return (
<div ref={boxRef} style={{ height: height || 'auto' }}>
{children}
</div>
);
}
The useLayoutEffect measures the box and updates the height before the paint. The useEffect would let the box paint at the auto height, then set the measured height, and the user would see the resize.
The scroll pattern. The component scrolls the element to the position before the paint.
function Chat({ messages }) {
const listRef = useRef(null);
useLayoutEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [messages]);
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
}
The useLayoutEffect scrolls the list to the bottom before the paint. The useEffect would let the list paint at the top, then scroll, and the user would see the jump.
The tooltip pattern. The component positions the tooltip relative to the trigger before the paint. The pattern is the same as the measurement.
The animation pattern. The component sets the initial transform before the animation starts.
function SlideIn({ children }) {
const [offset, setOffset] = useState(100);
const ref = useRef(null);
useLayoutEffect(() => {
setOffset(0);
}, []);
return (
<div
ref={ref}
style={{
transform: `translateX(${offset}px)`,
transition: 'transform 0.3s',
}}
>
{children}
</div>
);
}
The useLayoutEffect sets the offset to 0 before the paint, and the transition animates from the initial 100 to the 0. The useEffect would let the element paint at the 100, then set the 0, and the animation would start from the painted 100.
The SSR warning. The useLayoutEffect produces the warning in the server-side rendering, because the server has no DOM. The React documentation recommends the useEffect for the components that may be server-rendered, and the useLayoutEffect for the client-only components. The useIsomorphicLayoutEffect is the pattern that uses the useLayoutEffect on the client and the useEffect on the server:
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
The custom hook selects the useLayoutEffect when the window is defined (the client) and the useEffect when it is not (the server). The pattern is the standard for the libraries that support the SSR.
The layout-thrashing pattern. The useLayoutEffect that reads the DOM and writes the DOM repeatedly causes the layout thrashing: the browser recomputes the layout on every read after the write. The pattern is to batch the reads and the writes, and to use the requestAnimationFrame for the deferred work.
useLayoutEffect(() => {
// The reads first.
const heights = items.map((item) => item.ref.current.offsetHeight);
// The writes after.
items.forEach((item, i) => {
item.ref.current.style.height = `${heights[i]}px`;
});
}, [items]);
The reads first, then the writes, is the pattern that avoids the thrashing.
Complete Example Session
// ============================================
// PART 1: THE TOOLTIP WITH useLayoutEffect
// ============================================
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ targetRef, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const target = targetRef.current;
const tooltip = tooltipRef.current;
if (!target || !tooltip) return;
const targetRect = target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
setPosition({
top: targetRect.top - tooltipRect.height - 8,
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
});
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
>
{children}
</div>
);
}
// The tooltip is painted at the correct position the first time.
// The useEffect would let it paint at the top: 0, left: 0 first.
// ============================================
// PART 2: THE SAME WITH useEffect (THE FLICKER)
// ============================================
function TooltipFlicker({ targetRef, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useEffect(() => {
// The same measurement.
setPosition({ ... });
}, [targetRef]);
return <div ref={tooltipRef} style={{ top: position.top }}>{children}</div>;
}
// The tooltip paints at the top: 0, then moves to the position.
// The user sees the flicker.
// ============================================
// PART 3: THE SCROLL
// ============================================
function Chat({ messages }) {
const listRef = useRef(null);
useLayoutEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [messages]);
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
}
// The list is painted scrolled to the bottom.
// ============================================
// PART 4: THE MEASUREMENT
// ============================================
function AutoSizeBox({ children }) {
const [height, setHeight] = useState(0);
const boxRef = useRef(null);
useLayoutEffect(() => {
if (boxRef.current) {
setHeight(boxRef.current.getBoundingClientRect().height);
}
}, [children]);
return (
<div ref={boxRef} style={{ height: height || 'auto' }}>
{children}
</div>
);
}
// The box is painted at the measured height.
// ============================================
// PART 5: THE ANIMATION INITIAL STATE
// ============================================
function SlideIn({ children }) {
const [offset, setOffset] = useState(100);
const ref = useRef(null);
useLayoutEffect(() => {
setOffset(0);
}, []);
return (
<div
ref={ref}
style={{
transform: `translateX(${offset}px)`,
transition: 'transform 0.3s',
}}
>
{children}
</div>
);
}
// The element is painted at the 0, and the transition starts
// from the 100.
// ============================================
// PART 6: THE THEME FLICKER
// ============================================
function ThemeProvider({ theme, children }) {
useLayoutEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);
return children;
}
// The theme is applied before the paint.
// The useEffect would let the old theme paint, then the new one.
// ============================================
// PART 7: THE SSR WARNING
// ============================================
// The useLayoutEffect in the SSR produces:
// "useLayoutEffect does nothing on the server, because its effect
// cannot be encoded into the server renderer's output format."
// ============================================
// PART 8: THE useIsomorphicLayoutEffect
// ============================================
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
// The client uses the useLayoutEffect, and the server uses the
// useEffect. The warning is gone.
// ============================================
// PART 9: THE LAYOUT THRASHING
// ============================================
function BatchingExample({ items }) {
const refs = useRef([]);
useLayoutEffect(() => {
// The reads first.
const heights = refs.current.map(
(ref) => ref?.getBoundingClientRect().height ?? 0
);
// The writes after.
refs.current.forEach((ref, i) => {
if (ref) {
ref.style.height = `${heights[i]}px`;
}
});
}, [items]);
return (
<div>
{items.map((item, i) => (
<div key={item.id} ref={(el) => (refs.current[i] = el)}>
{item.name}
</div>
))}
</div>
);
}
// The reads and the writes are batched to avoid the thrashing.
// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================
function Dropdown({ triggerRef, items, open, onClose }) {
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
const dropdownRef = useRef(null);
useLayoutEffect(() => {
if (!open) return;
const trigger = triggerRef.current;
const dropdown = dropdownRef.current;
if (!trigger || !dropdown) return;
const triggerRect = trigger.getBoundingClientRect();
const dropdownRect = dropdown.getBoundingClientRect();
const spaceBelow = window.innerHeight - triggerRect.bottom;
const spaceAbove = triggerRect.top;
const top =
spaceBelow >= dropdownRect.height || spaceBelow >= spaceAbove
? triggerRect.bottom + 4
: triggerRect.top - dropdownRect.height - 4;
setPosition({
top,
left: triggerRect.left,
width: triggerRect.width,
});
}, [open, triggerRef]);
useEffect(() => {
if (!open) return;
function handleClickOutside(event) {
if (!dropdownRef.current?.contains(event.target)) {
onClose();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open, onClose]);
if (!open) return null;
return (
<div
ref={dropdownRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
minWidth: position.width,
}}
>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// The useLayoutEffect positions the dropdown before the paint.
// The useEffect handles the click-outside subscription.
// The two hooks work together.
The ten parts show the tooltip with the useLayoutEffect, the same with the useEffect (the flicker), the scroll, the measurement, the animation initial state, the theme flicker, the SSR warning, the useIsomorphicLayoutEffect, the layout thrashing, and the complete example.
Quick Reference
The Two Hooks
| Aspect | useLayoutEffect | useEffect |
|---|---|---|
| The timing | After the DOM mutation, before the paint | After the paint |
| The blocking | Synchronous; blocks the paint | Asynchronous; does not block |
| The DOM read | Valid | Valid |
The setState | The synchronous re-render | The asynchronous re-render |
| The SSR | The warning | The supported |
| The use case | The measurement, the scroll, the position | The data, the subscriptions, the logging |
The Timing Order
| Order | Phase |
|---|---|
| 1 | The render |
| 2 | The DOM mutation |
| 3 | The useLayoutEffect |
| 4 | The paint |
| 5 | The useEffect |
The Patterns
| Pattern | The hook |
|---|---|
| The tooltip position | useLayoutEffect |
| The scroll to the bottom | useLayoutEffect |
| The element measurement | useLayoutEffect |
| The animation initial state | useLayoutEffect |
| The theme application | useLayoutEffect |
| The data fetching | useEffect |
| The event subscription | useEffect |
| The analytics logging | useEffect |
The SSR
| Approach | The behavior |
|---|---|
The useLayoutEffect on the server | The warning |
The useEffect on the server | The no-op |
The useIsomorphicLayoutEffect | The client: the layout; the server: the passive |
The Anti-Patterns
| The anti-pattern | The problem |
|---|---|
| The heavy computation | The paint is blocked |
| The layout thrashing | The repeated read-write |
| The data fetching | The paint is blocked |
| The subscription | The timing is the wrong |
Best Practices
✅ Do This:
// Use the useLayoutEffect for the pre-paint measurement
useLayoutEffect(() => {
setPosition(measure(element));
}, []); // ✅
// Use the useEffect for the data fetching
useEffect(() => {
fetchData().then(setData);
}, []); // ✅
// Use the useIsomorphicLayoutEffect for the SSR
const useIsomorphicLayoutEffect = typeof window !== 'undefined'
? useLayoutEffect : useEffect; // ✅
// Batch the reads and the writes
const heights = refs.map((r) => r.offsetHeight);
refs.forEach((r, i) => { r.style.height = `${heights[i]}px`; }); // ✅
// Use the useLayoutEffect for the scroll
useLayoutEffect(() => {
listRef.current.scrollTop = listRef.current.scrollHeight;
}, [messages]); // ✅
// Keep the layout effects small
// The blocking is the cost // ✅
❌ Don’t Do This:
// Don't use the useLayoutEffect for the data fetching
useLayoutEffect(() => {
fetchData().then(setData); // blocks the paint // ❌
}, []);
// Don't use the useLayoutEffect in the SSR
useLayoutEffect(() => {
// the warning // ❌
}, []);
// Don't do the layout thrashing
refs.forEach((r) => {
const h = r.offsetHeight; // the read
r.style.height = `${h}px`; // the write after each read // ❌
});
// Don't use the useLayoutEffect for the logging
useLayoutEffect(() => {
log('rendered'); // the timing is wrong // ❌
}, []);
// Don't forget the dependency array
useLayoutEffect(() => {
// runs on every render // ❌
});
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The SSR warning | The useLayoutEffect on the server | Use the useIsomorphicLayoutEffect |
| The slow paint | The heavy layout effect | Move the work to the useEffect |
| The layout thrashing | The interleaved read-write | Batch the reads then the writes |
| The stale measurement | The missing dependency | Add the dependency |
| The flicker | The useEffect instead of the layout | Use the useLayoutEffect |
| The infinite loop | The setState without the dependency | Add the correct dependencies |
| The effect runs on every render | The missing dependency array | Add the array |
Real-World Examples
1. The Tooltip
useLayoutEffect(() => {
setPosition(computePosition(target, tooltip));
}, [targetRef]);
2. The Scroll
useLayoutEffect(() => {
listRef.current.scrollTop = listRef.current.scrollHeight;
}, [messages]);
3. The Measurement
useLayoutEffect(() => {
setHeight(boxRef.current.getBoundingClientRect().height);
}, [children]);
4. The Animation
useLayoutEffect(() => {
setOffset(0);
}, []);
5. The Theme
useLayoutEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);
6. The Isomorphic
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
7. The Batching
const heights = refs.map((r) => r.offsetHeight);
refs.forEach((r, i) => { r.style.height = `${heights[i]}px`; });
8. The Data Fetch
useEffect(() => {
fetchData().then(setData);
}, []);
9. The Subscription
useEffect(() => {
const sub = subscribe();
return () => sub.unsubscribe();
}, []);
10. The Complete Dropdown
useLayoutEffect(() => {
setPosition(computeDropdownPosition(trigger, dropdown));
}, [open]);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
Visual
The Effect Timing
┌──────────────────────────────────────────────────────────────┐
│ THE EFFECT TIMING │
│ │
│ 1. The render │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The component function runs. │ │
│ │ The element tree is computed. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. The DOM mutation │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The DOM is updated. │ │
│ │ The layout is computed. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. The useLayoutEffect │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The effect runs synchronously. │ │
│ │ The DOM read is valid. │ │
│ │ The setState triggers the synchronous re-render. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. The paint │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The browser paints the DOM. │ │
│ │ The user sees the final state. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 5. The useEffect │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The effect runs asynchronously. │ │
│ │ The DOM is already painted. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Flicker
┌──────────────────────────────────────────────────────────────┐
│ THE FLICKER │
│ │
│ WITH the useEffect: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. The tooltip renders at the top: 0, left: 0. │ │
│ │ 2. The DOM is mutated. │ │
│ │ 3. The browser PAINTS the tooltip at the 0, 0. │ │
│ │ ← the user SEES the wrong position │ │
│ │ 4. The useEffect runs, measures, and setState. │ │
│ │ 5. The re-render moves the tooltip. │ │
│ │ 6. The browser paints the correct position. │ │
│ │ ← the user sees the JUMP │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH the useLayoutEffect: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. The tooltip renders at the top: 0, left: 0. │ │
│ │ 2. The DOM is mutated. │ │
│ │ 3. The useLayoutEffect runs, measures, and setState.│ │
│ │ 4. The synchronous re-render moves the tooltip. │ │
│ │ 5. The browser PAINTS the correct position. │ │
│ │ ← the user sees ONLY the correct position │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The useLayoutEffect eliminates the flicker. │
│ │
└──────────────────────────────────────────────────────────────┘
The SSR Warning
┌──────────────────────────────────────────────────────────────┐
│ THE SSR WARNING │
│ │
│ THE SERVER: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The component renders to a string. │ │
│ │ There is NO DOM. │ │
│ │ │ │
│ │ The useLayoutEffect cannot run. │ │
│ │ The warning: │ │
│ │ "useLayoutEffect does nothing on the server, │ │
│ │ because its effect cannot be encoded into the │ │
│ │ server renderer's output format." │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE CLIENT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The DOM exists. │ │
│ │ The useLayoutEffect runs normally. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE FIX: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const useIsomorphicLayoutEffect = │ │
│ │ typeof window !== 'undefined' │ │
│ │ ? useLayoutEffect │ │
│ │ : useEffect; │ │
│ │ │ │
│ │ The client uses the layout effect, and the server │ │
│ │ uses the passive effect. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Layout Thrashing
┌──────────────────────────────────────────────────────────────┐
│ THE LAYOUT THRASHING │
│ │
│ ❌ THE INTERLEAVED: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ refs.forEach((r) => { │ │
│ │ const h = r.offsetHeight; // the READ │ │
│ │ r.style.height = `${h}px`; // the WRITE │ │
│ │ }); │ │
│ │ │ │
│ │ The browser recomputes the layout on every read │ │
│ │ after the write. The forced reflow. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ✅ THE BATCHED: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ // The reads first. │ │
│ │ const heights = refs.map((r) => r.offsetHeight); │ │
│ │ │ │
│ │ // The writes after. │ │
│ │ refs.forEach((r, i) => { │ │
│ │ r.style.height = `${heights[i]}px`; │ │
│ │ }); │ │
│ │ │ │
│ │ The browser computes the layout once. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useLayoutEffect | The synchronous effect before the paint |
useEffect | The asynchronous effect after the paint |
| The timing | The render → the DOM mutation → the layout effect → the paint → the passive effect |
| The blocking | The useLayoutEffect blocks the paint |
| The DOM read | The valid in both |
The setState | The synchronous re-render in the layout |
| The SSR | The warning for the layout |
The useIsomorphicLayoutEffect | The client/server switch |
| The measurement | The layout effect |
| The data fetching | The passive effect |
Key takeaways:
useLayoutEffectruns synchronously after the DOM is mutated but before the browser paints. It is the last chance to read the DOM and change the output before the user sees it. TheuseEffectruns asynchronously after the paint .- The
useLayoutEffecteliminates the flicker. The tooltip that measures its position, the list that scrolls to the bottom, and the animation that starts from the computed state are the cases. TheuseEffectwould let the intermediate state paint, and the user would see the jump . - The
useLayoutEffectblocks the paint. The browser waits for the effect to finish. A slow layout effect — the heavy measurement, the layout thrashing — makes the page feel sluggish. TheuseLayoutEffectshould be the small and the fast . - The
useLayoutEffectcan trigger the synchronous re-render. ThesetStatein the layout effect re-renders the component before the paint, and the user sees only the final state. TheuseEffect‘ssetStatere-renders after the paint, and the intermediate state is visible . - The
useLayoutEffectproduces the SSR warning. The server has no DOM, and the effect cannot run. TheuseIsomorphicLayoutEffectselects theuseLayoutEffecton the client and theuseEffecton the server . - The layout thrashing is the anti-pattern. The interleaved DOM read and write forces the browser to recompute the layout on every read. The fix is to batch the reads first and the writes after .
- The rule of thumb is to use the
useEffectby default. TheuseLayoutEffectis for the effects that read the DOM and update the state based on the read. The data fetching, the subscriptions, and the logging are the passive effects . - The profiler is the tool. The React DevTools Profiler shows the render and the commit timings. The
useLayoutEffectthat takes the long time is the paint block, and the optimization is the move to theuseEffector the reduction of the work .
Remember: useLayoutEffect is the synchronous hook for the pre-paint DOM reads. It runs after the DOM mutation and before the paint, it can measure the element and update the state, and the user never sees the intermediate state. The useEffect is the asynchronous hook for the post-paint work, and it is the default. The useLayoutEffect blocks the paint, and it should be the small and the fast. The useIsomorphicLayoutEffect handles the SSR. The result is the flicker-free rendering, which is the foundation of the polished UI.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!