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.
useLayoutEffect
?
What is Based on the definition from the React documentation:
useLayoutEffect
is a version ofuseEffect
that fires before the browser repains the screen.
useLayoutEffect
works?
How 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
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.
useLayoutEffect
?
When to use 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.
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:
- Tooltip renders with the initial tooltipHeight = 0 (so the tooltip may be wrongly positioned).
- React places it in the DOM and runs the code in useLayoutEffect.
- Your useLayoutEffect measures the height of the tooltip content and triggers an immediate re-render.
- Tooltip renders again with the real tooltipHeight (so the tooltip is correctly positioned).
- React updates it in the DOM, and the browser finally displays the tooltip.
Conclusion
- Despite the fact that
useLayoutEffect
is not used as often asuseEffect
, it's a powerful hook that can be useful in some specific cases.