Understanding uncommon React hooks - useLayoutEffect

Published on

In React, we have a set of built-in hooks that allow us to add features to our functional components. There're some common hooks that are very straightforward to use such as useState, useEffect, etc. However, there're also some uncommon hooks that are not used as often as the common ones but it comes very handy in some specific cases. In this series of blog posts, I want to dig into some of these uncommon hooks and explain how they work and where they can be useful.

In this first post, we'll take a look at the useLayoutEffect hook.

What is useLayoutEffect?

Based on the definition from the React documentation:

useLayoutEffect is a version of useEffect that fires before the browser repains the screen.

How useLayoutEffect works?

To understand how it works. Let's take a look at the rendering process in React.

In React, any screen update happens in three steps:

  • Triggering a render
  • Rendering the component
  • Commit the changes to the DOM
React rendering process

When you use the useEffect hook, the effect function is called after the browser has painted the screen but for the useLayoutEffect hook, the effect function is called before the browser paints the screen.

useEffect vs useLayoutEffect

When to use useLayoutEffect?

The useLayoutEffect hook is useful when you need to perform some DOM manipulations that need to happen before the browser paints the screen. For example, you might want to measure the size of an element before it's render on the screen.

Here's an example of using useLayoutEffect to avoid a "flickering" effect when rendering a tooltip taken from the React documentation.

See demo

The code for the demo is as follows, first create a tooltip component:

export default function Tooltip({ children, targetRect }: TooltipProps) {
  const ref = useRef<HTMLElement>(null)
  const [tooltipHeight, setTooltipHeight] = useState(0)

  // This artificially slows down rendering
  const now = performance.now()
  while (performance.now() - now < 100) {
    // Do nothing for a bit...
  }

  // If you use `useLayoutEffect` here, the tooltip will be rendered 
  // without flickering
  useEffect(() => {
    if (ref.current === null) return
    const { height } = ref.current.getBoundingClientRect()
    setTooltipHeight(height)
  }, [])

  let tooltipX = 0
  let tooltipY = 0
  if (targetRect !== null) {
    tooltipX = targetRect.left
    tooltipY = targetRect.top - tooltipHeight
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  )
}

Create a button component that shows a tooltip when hovering over it:

function ButtonWithTooltip({ tooltipContent, ...rest }) {
  const [targetRect, setTargetRect] = useState<TooltipProps['targetRect']>(null)
  const buttonRef = useRef<HTMLButtonElement>(null)
  return (
    <>
      <button
        {...rest}
        className="rounded-md border px-2 py-1"
        ref={buttonRef}
        onPointerEnter={() => {
          if (buttonRef.current === null) return
          const rect = buttonRef.current.getBoundingClientRect()
          setTargetRect({
            left: rect.left,
            top: rect.top,
            right: rect.right,
            bottom: rect.bottom,
          })
        }}
        onPointerLeave={() => {
          setTargetRect(null)
        }}
      />
      {targetRect !== null && (
        <EffectTooltip targetRect={targetRect}>{tooltipContent}</EffectTooltip>
      )}
    </>
  )
}

In the given example of the tooltip component, we use the useEffect hook to measure the height of the tooltip content. Because the useEffect hook is called after the browser paints the screen, the tooltip will be rendered with a flickering effect. If we use useLayoutEffect instead, React guarantees that the effect function will be called before the browser paints the screen.

Also note that useLayoutEffect will block browser from painting.

Here is how the tooptip component works:

  1. Tooltip renders with the initial tooltipHeight = 0 (so the tooltip may be wrongly positioned).
  2. React places it in the DOM and runs the code in useLayoutEffect.
  3. Your useLayoutEffect measures the height of the tooltip content and triggers an immediate re-render.
  4. Tooltip renders again with the real tooltipHeight (so the tooltip is correctly positioned).
  5. React updates it in the DOM, and the browser finally displays the tooltip.

Conclusion

  • Despite the fact that useLayoutEffect is not used as often as useEffect, it's a powerful hook that can be useful in some specific cases.

Comments