Side Effects in React: useLayoutEffect vs useEffect

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:

  1. A callback function containing the side effect logic.

  2. 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:

  1. The rectangle appears in red (its initial color).

  2. After the render, useEffect updates it to green and adds a top margin.

  3. 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 from useEffect.

  • 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! 🚀