useLayoutEffect - when to use it?

I recently attended an interview for React JS and the one question that I answered without actually ever implementing it was about the useLayoutEffect hook. I have heard about it from my colleague who had mentioned using it for some animation. I just looked for its definition then and it never occurred to me to implement it or to play around it until the day of the interview, where I could only answer what I read and couldn't really code to put out an example of it. So, after spending a good two hours on it, I am sharing the code snippets and explaining them here for an insight into the useLayoutEffect hook and how it is different from the useEffect hook, with examples.

useLayoutEffect is a React Hook that is similar to the useEffect Hook, but it runs synchronously immediately after all DOM mutations before the browser has a chance to paint those changes on the screen. This is useful for performing actions that depend on the layout or dimensions of the DOM elements, such as animating transitions or calculating measurements.

The signature useLayoutEffect is the same as useEffect: it takes two arguments, a function and an optional array of dependencies. The function argument will be executed after the initial render and after every update to the component, while the dependency array determines whether or not the effect should be re-run based on changes to its dependencies.

Here is an example of using useLayoutEffect to perform a simple animation:

import { useState, useLayoutEffect } from 'react';

function App() {
  const [show, setShow] = useState(false);

  useLayoutEffect(() => {
    const el = document.getElementById('box');
    el.style.opacity = '1';
    el.style.transform = 'translateX(0)';
  }, [show]);

  return (
    <div>
      <button onClick={() => setShow(!show)}>Toggle Box</button>
      {show && <div id="box" style={{ opacity: 0, transform: 'translateX(-50px)' }}>This is a box.</div>}
    </div>
  );
}

In this example, we are using useLayoutEffect to update the styles of the #box element when the show state changes. The useLayoutEffect hook runs synchronously after every update, so the DOM changes will be immediately reflected in the browser, allowing for smooth animations without flickering or delays.

It is important to note that because useLayoutEffect runs synchronously, it can potentially cause performance issues or jank if the effect takes too long to execute. In these cases, it may be better to use the useEffect Hook instead, which runs asynchronously after the browser has painted the changes on the screen. However, for certain use cases such as animations or layout-dependent effects, useLayoutEffect can be a useful tool for achieving a smooth and responsive user experience.

  1. Updating CSS Styles
import { useState, useEffect, useLayoutEffect } from 'react';

function App() {
  const [color, setColor] = useState('red');

  useEffect(() => {
    const el = document.getElementById('box');
    el.style.backgroundColor = color;
  }, [color]);

  useLayoutEffect(() => {
    const el = document.getElementById('box');
    el.style.borderColor = color;
  }, [color]);

  return (
    <div id="box" style={{ width: '100px', height: '100px', border: '5px solid black' }}>
      This is a box.
    </div>
  );
}

In this example, we are using useEffect to update the background colour of the #box element whenever the colour state changes, and we are using useLayoutEffect to update the border colour of the same element. Because useLayoutEffect runs synchronously before the browser has a chance to paint the changes, the border colour will be updated before the background colour, resulting in a brief flicker where the border colour is visible before the background colour is updated.

  1. Measuring Element Dimensions
import { useState, useEffect, useLayoutEffect } from 'react';

function App() {
  const [width, setWidth] = useState(null);

  useEffect(() => {
    const el = document.getElementById('box');
    setWidth(el.clientWidth);
  }, []);

  useLayoutEffect(() => {
    const el = document.getElementById('box');
    setWidth(el.clientWidth);
  }, []);

  return (
    <div id="box" style={{ width: '50%' }}>
      This is a box. Its width is {width}px.
    </div>
  );
}

In this example, we are using both useEffect and useLayoutEffect to measure the width of the #box element and update the width state accordingly. Because useEffect runs asynchronously after the browser has painted the changes, it may not always reflect the most up-to-date measurements of the element. In contrast, useLayoutEffect runs synchronously before the browser paints the changes, ensuring that the width state is always up-to-date with the latest measurements.

Overall, the choice between useEffect and useLayoutEffect depends on the specific use case and the desired behavior of the effect. If the effect relies on the layout or dimensions of DOM elements or needs to perform synchronous updates to the DOM, useLayoutEffect may be more appropriate. Otherwise, useEffect is generally a safer and more performant choice.