Understanding the difference between React useEffect and useLayoutEffect hooks

Confused over useEffect hook and useLayoutEffect hooks?

In this article, we will look into how useEffect and useLayoutEffect differ from each other and their use-cases.

Before we dive into the difference between the two it is worth mentioning that both the hooks -

  • Handle side-effects in React
  • Have an identical signature

Let’s consider a simple example of a counter.

import { useEffect, useState, useLayoutEffect } from 'react';

function App() {
 const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('useEffect is fired');
    //Side effect
  }, [count]);

return(
 <div>
    <h1>Count: {count} </h1>
    <button onClick={() => setCount(count + 1)}>Increment count</button>
 </div>
 )
}

When the component is mounted Count: {count} appears on the screen. When the button is clicked the counter is incremented.

The same behavior is seen when useLayoutEffect is replaced with useEffect.

How is useEffect different from useLayoutEffect?

The difference between them is the timing of their invocation.

This diagram will help to understand the flow better.

Image Credits : hook-flow

useEffect runs asynchronously after a render is painted to the screen, unblocking the browser paint process.

So that looks like:

  1. We cause a render somehow (through the state, or the parent re-renders or context change). In our case by clicking on the Increment button.
  2. React updates the state internally. Counter value gets updated to 1.
  3. React handles the DOM mutation. <h1>Count: 0 </h1> gets changed to <h1>Count: 1 </h1>
  4. The browser paints this DOM change to the browser’s screen. In our case Count: 1 is painted on the screen.
  5. The useEffect function is fired after the browser has painted the DOM changes. Prints useEffect is fired.

However, useLayoutEffect fires synchronously after all DOM mutations.

So that looks like:

  1. We cause a render somehow (through the state, or the parent re-renders or context change). In our case by clicking on the Increment button.
  2. React updates the state internally. Counter value gets updated to 1.
  3. React handles the DOM mutation. <h1>Count: 0 </h1> gets changed to <h1>Count: 1 </h1>
  4. useLayoutEffect is fired immediately after the DOM mutations.
  5. The browser paints this DOM change to the browser’s screen. In our case Count: 1 is painted on the screen.

In simple words, useLayoutEffect doesn’t care whether the browser has painted the DOM changes or not. It triggers the function right after the DOM mutations are computed.

Use-cases for useEffect and useLayoutEffect

Deciding which hook to choose between useEffect and useLayoutEffect may be tricky.

Understanding the situation is the key!

99% of the time useEffect is what we want to use. Most of the time we are fetching data and setting up event handlers that do not need to happen immediately. It also does not affect page appearance. For all such cases, we should use the useEffect hook.

So what is the right time to use useLayoutEffect?

If our effect will mutate the DOM (like getting the scroll position or other styles for an element) or involves animation prefer useLayoutEffect over useEffect.

Reason:

useEffect hook is called after the screen is painted. Therefore mutating the DOM again immediately after the screen has been painted, will cause a flickering effect if the mutation is visible to the client.

Let’s take the example of a tooltip. Here we are taking measurements of the button from DOM and manipulating the position of the tooltip.

const App = () => {
  const [showToolTip, setShowTooltip] = useState(false);
  const buttonRef = useRef();
  const tooltipRef = useRef();

  useEffect(() => {
    if (buttonRef.current == null || tooltipRef.current == null) return;

    const { left, top } = buttonRef.current.getBoundingClientRect();
    tooltipRef.current.style.left = `${left + 120}px`;
    tooltipRef.current.style.top =  `${top - 20}px`;
  }, [showToolTip]);

  return (
    <div>
      <button ref={buttonRef}
        onClick={() => setShowTooltip(prevState => !prevState)}>
        Toggle tooltip
      </button>

      {showToolTip &&
        (<div ref={tooltipRef} 
        style=
        {{ position: 'absolute', 
           border: '1px solid gray', 
           padding: '20px', 
           backgroundColor: 'lightgray'
        }}
        >
          This is a Tooltip!
        </div>)}
    </div>
  )
};

We see a flicker for a small duration after the button is clicked. This is because the useEffect is fired asynchronously.

Replacing useEffect with useLayoutEffect removes the flicker.

useEffect in React 18

Running the same useEffect code with React 18 does not cause flickering.

Yes, this is true!

In React 18, useEffect fires synchronously when it’s the result of a discrete input.

Now, what is discrete input?

A discrete input is a type of event where the result of one event can affect the behavior of the next, like clicks.

In our case, clicking on the button, fires useEffect synchronously avoiding flickering.

To read about the changes in useEffect in React 18 check-out this link.

Wrap up

  • Replacing the useEffect hook with useLayoutEffect may not have a noticeable effect in simple apps, it is strongly advised not to do so.
  • useLayoutEffect can be expensive to run, so it should be used only when required.
  • In React 18, only for discrete events useEffect will fire synchronously. For most of the cases(non-discrete events), React will defer the effects until after paint.

Refer to this link to know about useLayoutEffect and server-side rendering.

Need help on your Ruby on Rails or React project?

Join Our Newsletter