44
loading...
This website collects cookies to deliver better user experience
useEffect
should run after paint to prevent blocking the update. But did you know it’s not really guaranteed to fire after paint? Updating state in useLayoutEffect
makes every useEffect
from the same render run before paint, effectively turning them into layout effects. Confusing? Let me explain.useLayoutEffect
useEffect
Although useEffect is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.
useLayoutEffect
, then the effect must be flushed before that update, which is before paint. Here’s a timeline:useLayoutEffect
useEffect
useLayoutEffect
from update 2useEffect
from update 2useEffect
, because updating state updates the DOM, and doing so after paint leaves the user with one stale frame, resulting in noticeable flickering.300px
. We need real DOM to measure the input, so we need some effect. We also don’t want the icon to appear / disappear after one frame, so the initial measurement goes into useLayoutEffect
:const ResponsiveInput = ({ onClear, ...props }) => {
const el = useRef();
const [w, setW] = useState(0);
const measure = () => setW(el.current.offsetWidth);
useLayoutEffect(() => measure(), []);
useEffect(() => {
// don't take this too seriously, say it's a ResizeObserver
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, []);
return (
<label>
<input {...props} ref={el} />
{w > 200 &&
<button onClick={onClear}>clear</button>}
</label>
);
};
addEventListener
until after paint with useEffect
, but the state update in useLayoutEffect
forces it to happen before paint (see sandbox):useEffect
is not affected by useLayoutEffect
state update:useLayoutEffect
. But are you sure none of the custom hooks it uses do that?useContext
or a parent re-render.useEffect
, and a memo()
. But a uLE state update in the parent still appears to flush child effects.useLayoutEffect
, but that’s superhuman. The best advice is not to rely on useEffect
to fire after paint, just like useMemo
does not guarantee 100% stable reference. If you want the user to see something painted for one frame, useEffect
is not the way to do it — try double requestAnimationFrame
or do the postMessage trick yourself.useEffect
. You test it, and, aha!, no flickering. Bad news — maybe it’s the result of a state update before paint. Move some code around, and it will flicker.useEffect
vs useLayoutEffect
guidelines to the letter, we could split one logical side-effect into a layout effect to update the DOM, and a “delayed” effect, like we’ve done in our ResponsiveInput
example:// DOM update = layout effect
useLayoutEffect(() => setWidth(el.current.offsetWidth), []);
// subscription = lazy logic
useEffect(() => {
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, []);
useEffect
does fire after paint, are you 100% sure the element won’t resize between the effects? I’m not. Leaving all size-tracking logic in a single layoutEffect
here is safer, cleaner, has the same amount of pre-paint work, and gives React one less effect to manage — pure win:useLayoutEffect(() => {
setWidth(el.current.offsetWidth);
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, []);
useEffect
is a worse place to update state, because flickering is poor UX, and UX is more important than performance. Updating state during render looks dangerous. If you can, try to come up with a state model that doesn’t rely on effects, but I don’t know how to invent “good” state models on command.useLayoutEffect
causing trouble, consider bypassing state update and mutating DOM directly. That way, react doesn’t schedule an update, and needn’t flush effects eagerly. We could try:const clearRef = useRef();
const measure = () => {
// No worries react, I'll handle it:
clearRef.current.display = el.current.offsetWidth > 200 ? null : 'none';
};
useLayoutEffect(() => measure(), []);
useEffect(() => {
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, []);
return (
<label>
<input {...props} ref={el} />
<button ref={clearRef} onClick={onClear}>clear</button>
</label>
);
useState
, and we just got one more reason to skip react updates. Still, manually managing DOM updates is complicated and error-prone, so reserve this trick for performance-critical situations — very hot components or super-heavy useEffects.useEffect
sometimes executes before paint. A frequent cause is updating state in useLayoutEffect
— it requests a re-render before paint, and the effect must run before that re-render. What this means for us:useLayoutEffect
is not good for app performance. Try not to do that, but sometimes there is no good alternative.useEffect
to fire after paint.useEffect
will cause a visible flicker — maybe you don’t see it because of a layout effect updating state.useLayoutEffect
into useEffect
for performance makes no sense if you set state in the layout effect part.