Definition
When a child component tries to set an attribute on, or measure the size of, "a DOM element the parent passed down," there are times when that DOM comes back as null even though it's clearly rendered on screen. Use this concept when you want to understand that timing problem and avoid it safely.
React commit (the phase that reflects changes into the real browser DOM after the virtual DOM computation is done) generally connects from the child DOM node toward the parent (child-first, attaching from the lower nodes of the tree upward). Therefore, at the point the child component's useLayoutEffect (an effect that runs synchronously right after the DOM change, before painting to screen) runs, the ref.current the parent passed (the reference box pointing to the parent DOM element) may still be null. To write attributes to, or measure, an ancestor DOM element, you must defer to useEffect (a passive effect that runs after painting to screen), or change the target to a callback ref·your own DOM ref.
Understanding in context
When a child component knows "whether the document is empty" and tries to set an attribute like data-empty on the container ref the parent passed, there are cases where the store has a value but only ref.current is null. Because React attaches the DOM from the bottom up, when the child useLayoutEffect runs the parent ref may still be pre-connection. It looks like a bug, but it's a commit order issue.
Why it's needed
If the child writes to the parent ref in useLayoutEffect, like containerRef.current.setAttribute('data-empty', …), it becomes a silent no-op because of commit order. You're left with a debug log where the data·store snapshot exists but el === null. Writing to an ancestor ref "because the layout effect is faster" is a common pitfall.
How it works
A React update roughly follows this order.
- Trigger — state/props change
- Render — compute the Virtual DOM
- Commit — real DOM change (usually child-first)
- Layout effects —
useLayoutEffect(before paint, synchronous) - Paint
- Passive effects —
useEffect(after commit completes)
| Phase | Ancestor ref.current | Suitable use |
|---|---|---|
Child useLayoutEffect | Often null | Own DOM measurement·sync adjustment |
Child useEffect | Usually connected | Writing to ancestor ref·non-urgent side effects |
| Callback ref (parent) | Parent controls directly | Parent receives a child signal to paint DOM |
The attribute may be attached one frame late. For flags that aren't visually critical (e.g. an empty-document indicator), this is often acceptable.
Practical application
- Writing to an ancestor/container ref is done in the child
useEffect. - If synchronous measurement is truly required, move the ref target to your own DOM, or have the parent paint directly with a callback ref.
- To avoid container re-renders, isolate just the flag with a
return nullleaf + external store subscription. - On a Strict Mode double-mount, clear the attribute in effect cleanup.
Trade-offs
useEffectis after paint, so the attribute·measurement is a tick late — if flicker shows, review a callback ref or parent-side logic.useLayoutEffectblocks the main thread — don't use it for accessing an ancestor ref.- Portal children are a different commit subtree — the ancestor ref rules can differ.
When not to use
- When you must read your own DOM's size before paint — move the target ref to your own node, then use
useLayoutEffect. - A rare special wrapper whose API contract guarantees it's right after the parent ref was connected via a callback ref.
Common mistakes
- Using
useLayoutEffecton an ancestor ref "to render faster." containerRef.currentis null but logic is skipped without an early return, so the cause diagnosis is deferred.- Measuring an ancestor's scrollHeight in a child layout effect — wrong 0 or an exception.
- Putting a doc that changes via Context into the Provider value, so the container re-renders on every commit — a performance issue separate from ref timing.
Related concepts
- critical-rendering-path — DOM·style·layout·paint order
- react-context-render-granularity — separating container vs leaf subscribers
- fixed-position-floating-ui-scroll — a different timing issue in ref-based coordinate UI