React Hooks Part 7: useImperativeHandle and useLayoutEffect
Two escape hatches most tutorials skip: useImperativeHandle for exposing a controlled API from a child component, and useLayoutEffect for DOM measurements that must happen before the browser paints.
React Hooks: A Complete Guide
A four-part deep-dive into React Hooks — from managing state with useState to squeezing performance out of useCallback and useMemo. Each article is standalone yet builds on the previous, giving you both quick reference and progressive mastery.
- 5 React Hooks Part 5: Sharing State with useContext
- 6 React Hooks Part 6: useId, useTransition, and useDeferredValue
- 7 React Hooks Part 7: useImperativeHandle and useLayoutEffect
- 8 React Hooks Part 8: Rules, Anti-Patterns, and the ESLint Plugin
- 9 React Hooks Part 9: Testing Hooks with React Testing Library
Table of Contents
- Two hooks you rarely need — until you do
- useImperativeHandle — expose a controlled API from a child
- The pattern
- Why not just expose the raw DOM ref?
- useLayoutEffect — synchronous DOM work before paint
- The difference in timing
- When you need it: measuring the DOM
- The SSR caveat
- When to use each
- Key takeaways
Two hooks you rarely need — until you do
Most React code never requires useImperativeHandle or useLayoutEffect. But when you're building component libraries, animating DOM nodes, or integrating third-party SDKs, they become essential.
useImperativeHandle — expose a controlled API from a child
By default, a parent can't call methods on a child component. useImperativeHandle changes that by letting a child decide exactly which methods to expose via a ref.
It must be used together with forwardRef.
The pattern
import { forwardRef, useImperativeHandle, useRef } from 'react'
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null)
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus()
},
clear() {
if (inputRef.current) inputRef.current.value = ''
},
}))
return <input ref={inputRef} {...props} />
})
The parent gets a ref that only has focus and clear — not the raw DOM node:
function Form() {
const inputRef = useRef(null)
return (
<>
<FancyInput ref={inputRef} placeholder="Type here…" />
<button onClick={() => inputRef.current.focus()}>Focus</button>
<button onClick={() => inputRef.current.clear()}>Clear</button>
</>
)
}
Why not just expose the raw DOM ref?
If you forward the raw DOM ref, you give the parent unrestricted access — they can modify styles, read any property, call any DOM method. useImperativeHandle defines a contract: "here's what you're allowed to do."
This is especially important in design system components like <Modal>, <Tooltip>, or <DatePicker> where you want callers to use open() / close() rather than poking the DOM directly.
useLayoutEffect — synchronous DOM work before paint
useLayoutEffect has the same signature as useEffect, but it fires synchronously after DOM mutations and before the browser paints.
useLayoutEffect(() => {
// DOM is updated, browser hasn't painted yet
}, [dependencies])
The difference in timing
useEffect: render → commit → paint → effect
useLayoutEffect: render → commit → effect → paint
When you need it: measuring the DOM
import { useState, useLayoutEffect, useRef } from 'react'
function Tooltip({ text, anchorRef }) {
const tooltipRef = useRef(null)
const [position, setPosition] = useState({ top: 0, left: 0 })
useLayoutEffect(() => {
const anchor = anchorRef.current.getBoundingClientRect()
const tooltip = tooltipRef.current.getBoundingClientRect()
setPosition({
top: anchor.bottom + 8,
left: anchor.left - tooltip.width / 2 + anchor.width / 2,
})
}, [text])
return (
<div
ref={tooltipRef}
style={{ position: 'fixed', top: position.top, left: position.left }}
>
{text}
</div>
)
}
If you used useEffect instead, the tooltip would briefly flash in the wrong position before jumping to the correct one — because the paint happens between the render and the effect. useLayoutEffect measures and repositions before the user sees anything.
The SSR caveat
useLayoutEffect emits a warning when run on the server (Next.js, Astro SSR) because the DOM doesn't exist during server rendering. The fix:
// Use useEffect on the server, useLayoutEffect in the browser
import { useEffect, useLayoutEffect } from 'react'
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
When to use each
| Hook | When |
|---|---|
useEffect |
Side effects that don't need to block painting |
useLayoutEffect |
DOM measurements, animations, tooltip positioning |
useImperativeHandle |
Exposing a controlled imperative API from a component |
Key takeaways
useImperativeHandle+forwardRefdefine a public API for a component — use it in library components, not in application code.useLayoutEffectfires synchronously before paint — reach for it only when you're reading DOM geometry.- For SSR compatibility, alias
useLayoutEffecttouseEffecton the server.