
In React, useEffect
is the primary way of handling side effects in function components. However, there’s another hook that often gets overlooked although it’s crucial for certain use cases: useLayoutEffect
. In this post, we'll explore what useLayoutEffect
is, how it differs from useEffect
, and when you should use it to ensure your components render correctly.
What is useLayoutEffect?
The useLayoutEffect
hook runs synchronously after React has performed all DOM mutations but before the browser has painted the updated layout on the screen. This timing gives it a unique place in the rendering cycle, allowing you to perform actions that depend on the DOM layout, such as measuring the position and size of elements before the user sees the result.
It is most commonly used when you need to measure or manipulate DOM elements prior to rendering to prevent visual inconsistencies.
Syntax
useLayoutEffect(setup, dependencies?)
setup
: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function.dependencies (optional)
: The optional list of dependencies referenced in the setup function.
useEffect vs useLayoutEffect
Both useEffect
and useLayoutEffect
share the same function signature and purpose: handling side effects in function components. However, they differ significantly in when they execute within the React rendering cycle.
useEffect
: Asynchronous and non-blocking. Executes after the component has rendered and the browser has painted the updates to the screen.useLayoutEffect
: Synchronous and blocking. Executes after React has made all DOM mutations but before the browser paints the updates.
Example Overview
Practical uses for useLayoutEffect
are limited, as useEffect
will be a better approach for most side effect management scenarios. However, there are times where useLayoutEffect
will work better than useEffect
. A potential use case is dynamically positioned tooltips.
💡 A “tooltip” in web development is a small, informative popup that appears when a user hovers over an element.
Implementing a tooltip is relatively straightforward. However, if you wanted to dynamically position the tooltip based on the available space, things would get somewhat complicated.
Imagine a tooltip that appears next to an element on hover, but its location needs to be decided based on the available space. For example, if there is enough space above the hovered element, the tooltip might be positioned on top of the element, and otherwise, it might be positioned below the element.
Calculating the position of the tooltip with the traditional useEffect
hook would cause a potentially noticable visual flicker (especially if the user’s computer is slow), because the element would have to be momentarily misplaced before it gets moved into position after the initial paint.
This is where useLayoutEffect
proves useful. Because it executes before the DOM paint, there will be no visual flickers or layout shifts that would otherwise happen with useEffect
.
Here is an overview of what we will be building:

Detailed Example
The App
component is simple, it’s just a container for 3 Boxes:
// App.jsx
import Box from "./Box"
const CONTAINER_STYLE = {
width: "500px",
display: "flex",
flexDirection: "column",
alignItems: "center",
}
export default function App() {
return (
<div style={CONTAINER_STYLE}>
<Box />
<Box />
<Box />
</div>
)
}
The Box
component is where we will need to handle the hover events. This is how the code would look like without the tooltip logic:
// Box.jsx (incomplete version)
import { useState, useRef } from "react"
const BOX_STYLE = {
border: "2px solid darkblue",
backgroundColor: "darkblue",
color: "white",
borderRadius: "5px",
padding: "3px",
margin: "10px",
width: "140px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}
export default function Box() {
return (
<div style={BOX_STYLE}>
Hover over me!
</div>
)
}
Some work needs to be done here:
- We need a ref to the box, so that we can calculate where the box is located in the document
- We need to handle hover events, so that we know when to show the tooltip
A box is essentially a singular div element, so we can easily get a ref to it:
// Box.jsx (incomplete version)
// ...
export default function Box() {
const ref = useRef(null)
return (
<div
style={BOX_STYLE}
ref={ref}
>
Hover over me!
</div>
)
}
We will be using this ref to get information about the box, which we will store in a state variable. This state variable will contain the box’ left, top and bottom values. The state variable will be set on hover, and it will be unset when the mouse leaves the element. The Tooltip
component will be conditionally rendered based on whether this state variable is null.
To handle hover events, we will be using the onPointerEnter
and onPointerLeave
HTML event attributes.
// Box.jsx (final version)
import { useState, useRef } from "react"
import Tooltip from "./Tooltip"
const BOX_STYLE = {
border: "2px solid darkblue",
backgroundColor: "darkblue",
color: "white",
borderRadius: "5px",
padding: "3px",
margin: "10px",
width: "140px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}
export default function Box() {
const [targetRect, setTargetRect] = useState(null) // state to track the target's position
const ref = useRef(null) // ref to access the Box' DOM node
return (
<>
<div
style={BOX_STYLE}
ref={ref}
// event handler for when the pointer enters the box
onPointerEnter={() => {
const rect = ref.current.getBoundingClientRect()
setTargetRect({
left: rect.left,
top: rect.top,
bottom: rect.bottom,
})
}}
// event handler for when the pointer leaves the box
onPointerLeave={() => {
setTargetRect(null)
}}
>
Hover over me!
</div>
{targetRect !== null && (
<Tooltip targetRect={targetRect}>Tooltip💡</Tooltip>
)}
</>
)
}
The Tooltip
component recieves the “target element” to decide its own location with useLayoutEffect
.
// Tooltip.jsx
import { useRef, useLayoutEffect, useState } from "react"
import { createPortal } from "react-dom"
const TOOLTIP_STYLE = {
position: "absolute",
pointerEvents: "none",
left: 0,
top: 0,
height: "20px",
border: "1px solid black",
borderRadius: "4px",
backgroundColor: "lightgrey",
padding: "5px 20px",
color: "black",
zIndex: 2,
}
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null)
const [tooltipHeight, setTooltipHeight] = useState(0)
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect() // adding a null check here for ref.current would be nice
setTooltipHeight(height)
}, [])
let tooltipX = 0
let tooltipY = 0
if (targetRect !== null) {
tooltipX = targetRect.left
tooltipY = targetRect.top - tooltipHeight
if (tooltipY < 0) {
// if target element's "top" is less than the tooltip's height, it won't fit above
console.info("Tooltip didn't fit above, so placing it below.")
tooltipY = targetRect.bottom
} else {
console.info("Tooltip fits above.")
}
}
return createPortal(
<div
style={{
...TOOLTIP_STYLE,
transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)`,
}}
ref={ref}
>
{children}
</div>,
document.body
)
}
This code may look intimidating at first sifht, but there is actually not much going on here. Information about the target element (the element that the tooltip is supposed to be displayed next to) is passed as a prop, which is an object containing three properties: left, top and bottom.
In essence, the space above the target element is compared with the tooltip’s height to decide whether the tooltip should be positioned above or below the target element.
This example uses a React portal (createPortal
API from react-dom
), because it is common practice to place popups and tooltips at the document body. React Portals allow us to render a component outside its parent hierarchy, which is especially useful for modals and tooltips. This helps avoid CSS overflow issues and keeps the tooltip positioned correctly relative to the viewport.
However, if you’re not familiar with portals, you can simply remove the portal. In this case, the code will work just fine without it:
// Tooltip.jsx without portal (not recommended)
// ...
return (
<div
style={{
...TOOLTIP_STYLE,
transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)`,
}}
ref={ref}
>
{children}
</div>
)
I will be writing a blog post dedicated to React portals in the future.
Once again, here is the final result:

Caveats
- The code inside
useLayoutEffect
blocks the browser from re-painting the screen. Therefore, if used unnecessarily, this can make your app slower. When possible, preferuseEffect
. - Keep in mind that
useLayoutEffect
does not run during server-side rendering because it relies on the DOM, which isn't available on the server. Stick touseEffect
when workign with SSR.
Source Code & Final Thoughts
The source code for the example project is available on GitHub. If this post helped you, a clap would be greatly appreciated! To stay updated with more content like this, consider following me.
For additional reads, feel free to explore my curated reading lists, where you can find more articles I’ve authored.