Measuring Before Painting: Why My Dropdown Needed useLayoutEffect

Introduction:
I've been building out the rich-text editor experience for KiroBit, focusing heavily on getting a smooth, Notion-style workflow. I wanted a unified formatting menu that triggers identically whether the user hits the / key or highlights text and right-clicks. I built out the component, populated it with all the TipTap formatting commands (headings, colors, code blocks), and wired up the event listeners to grab the cursor's X and Y coordinates to mount the menu at the anchor point.
Problem Statement:
When you build custom context menus, standard CSS position: absolute is a trap.
Everything worked perfectly when I tested it in the middle of a document. But the moment I typed / at the very bottom of a long note, or near the right edge of the screen, the UI broke. The formatting menu was blindly rendering down and to the right of the cursor, plunging straight off the edge of the viewport. Half the buttons were unclickable, and the overflow: hidden on my main container was ruthlessly clipping the menu out of existence. It completely ruined the typing flow.
Debug steps:
I realized this isn't just a CSS styling issue; it's a dynamic geometry problem. The application needs to be self-aware of its own viewport boundaries and the height of the menu it's about to render.
I wrote a calculation engine (getFormatMenuPlacement) that measures the space available. If window.innerHeight - anchor.y is less than the menu's height, the logic dictates the menu must "flip" and open upwards.
However, this introduced a classic React lifecycle race condition: How do you mathematically know the height of a dynamic menu before it actually renders to the screen? If I guessed a static height, the calculation would fail whenever I added new items to the menu.
To solve this, I leveraged useLayoutEffect. I first render the menu optimistically using an estimated height. useLayoutEffect fires synchronously after DOM mutations but before the browser actually paints the screen to the user. Inside that hook, I grab the useRef of the menu, measure its exact rendered height (getBoundingClientRect().height), and instantly recalculate the top position if it needs to open upwards.
The Code:
Here is the core logic that handles the measurement and placement before the browser paints:
const formatMenuPopoverRef = useRef(null)
const [formatMenuMeasuredPos, setFormatMenuMeasuredPos] = useState(null)
// 1. Initial calculation based on cursor anchor and screen bounds
const formatMenuPlacement = useMemo(
() => (menu ? getFormatMenuPlacement({ x: menu.x, y: menu.y }) : null),
[menu]
)
// 2. The DOM measurement trap
useLayoutEffect(() => {
if (!menu || !formatMenuPlacement?.opensAbove) {
setFormatMenuMeasuredPos(null)
return
}
const el = formatMenuPopoverRef.current
if (!el) return
// Measure the ACTUAL height of the DOM node right before it paints
const measuredH = el.getBoundingClientRect().height
const gap = 8 // Pixel gap from the anchor
// Calculate the precise 'flip' position upwards
let top = menu.y - measuredH - gap
const ih = window.innerHeight
top = Math.max(gap, Math.min(top, ih - measuredH - gap))
// Override the position
setFormatMenuMeasuredPos({ top, left: formatMenuPlacement.left })
}, [menu, formatMenuPlacement])
It’s one of those subtle engineering details that users never actively notice, but if it wasn't there, the app would feel incredibly cheap. Controlling the DOM paint cycle prevents any ugly UI flickering while ensuring the menu is always 100% accessible, no matter where you are typing.
