React’s data flow process is straightforward—it breaks down the user interface into components, builds a tree of elements, and updates the DOM when components change. However, some operations, such as interacting with an API, fall outside this flow. These operations are called side effects.
A side effect is any observable change that occurs after a function runs, apart from its return value. Common side effects include network requests, data fetching, and manual DOM manipulations.
In this article, we will explore how to handle side effects in React using useLayoutEffect
and compare it with useEffect
.
Prerequisites
To follow along, you should have:
A good understanding of functional programming with JavaScript and React.
Experience using the
useEffect
hook.
What is useLayoutEffect
?
According to React’s official documentation:
The signature is identical to
useEffect
, but it fires synchronously after all DOM mutations.
Understanding useEffect
Here’s the function signature of useEffect
:
useEffect(
// callback function,
[ /* dependencies */ ]
);
useEffect
takes two arguments:
A callback function containing the side effect logic.
A dependencies array, which triggers the callback when any listed variable changes. If the array is empty (
[]
), the effect runs only once when the component mounts.
Understanding useLayoutEffect
The function signature for useLayoutEffect
is identical:
useLayoutEffect(
// callback function,
[ /* dependencies */ ]
);
The difference lies in when they execute.
useLayoutEffect
vs useEffect
– The Difference
Both hooks share the same syntax and can often be used interchangeably, but there is one key difference: execution timing.
Execution Flow of useEffect
When a user triggers a re-render (e.g., by updating state or props), React updates the screen first, then runs the useEffect
callback asynchronously after the DOM updates.
Execution Flow of useLayoutEffect
In contrast, useLayoutEffect
runs synchronously before the DOM updates.
Let's illustrate this with an example:
const Comparison = () => {
// with useEffect
useEffect(() => {
console.log('This is useEffect');
}, []);
// with useLayoutEffect
useLayoutEffect(() => {
console.log('This is useLayoutEffect');
}, []);
return <div>Comparison</div>;
};
export default Comparison;
What Happens in the Console?
Since
useLayoutEffect
runs before the DOM updates, it logs first.After the DOM updates,
useEffect
runs asynchronously and logs second.
When to Use useLayoutEffect
Most of the time, useEffect
is preferred because it executes after the DOM updates, reducing performance bottlenecks. However, in some cases, useLayoutEffect
is necessary, especially when dealing with DOM measurements or visual updates.
Example: Rendering a Rectangle with useEffect
import React, { useEffect, useRef, useState } from 'react';
const RenderRectangle = () => {
const [display, setDisplay] = useState(false);
const rectangle = useRef();
useEffect(() => {
if (rectangle.current == null) return;
rectangle.current.style.backgroundColor = 'green';
rectangle.current.style.marginTop = '20px';
}, [display]);
return (
<div>
<h1>useEffect - Render Rectangle</h1>
<button
style={{ width: 100, height: 40, borderRadius: 5 }}
onClick={() => setDisplay(!display)}
>
{display ? 'Hide' : 'Show'}
</button>
{/* Rectangle */}
{display && (
<div
style={{ width: 100, height: 50, backgroundColor: 'red' }}
ref={rectangle}
></div>
)}
</div>
);
};
export default RenderRectangle;
Behavior in the Browser
When the button is clicked:
The rectangle appears in red (its initial color).
After the render,
useEffect
updates it to green and adds a top margin.This creates a visual flicker, as the color change happens after rendering.
Fixing the Flicker with useLayoutEffect
import React, { useLayoutEffect, useRef, useState } from 'react';
const RenderRectangle = () => {
const [display, setDisplay] = useState(false);
const rectangle = useRef();
useLayoutEffect(() => {
if (rectangle.current == null) return;
rectangle.current.style.backgroundColor = 'green';
rectangle.current.style.marginTop = '20px';
}, [display]);
return (
<div style={{ width: '100%', maxWidth: '800px', margin: 'auto' }}>
<h1>useLayoutEffect - Render Rectangle</h1>
<button
style={{ width: 100, height: 40, borderRadius: 5 }}
onClick={() => setDisplay(!display)}
>
{display ? 'Hide' : 'Show'}
</button>
{/* Rectangle */}
{display && (
<div
style={{ width: 100, height: 50, backgroundColor: 'red' }}
ref={rectangle}
></div>
)}
</div>
);
};
export default RenderRectangle;
Behavior After Using useLayoutEffect
Since
useLayoutEffect
runs before the DOM updates, the rectangle is immediately green with the correct margin.There is no flicker or transition glitch.
Conclusion
In this article, we explored:
How
useLayoutEffect
differs fromuseEffect
.When to use
useLayoutEffect
for smoother DOM updates.How to avoid UI glitches by choosing the right hook for the job.
For most cases, stick with useEffect
, as it ensures a non-blocking UI. However, when working with DOM measurements or layout-dependent styles, useLayoutEffect
provides a better user experience.
Want to try the examples in action? Check out the CodeSandbox demo here! 🚀