Refs, Portals & Escape Hatches
When declarative is not enough
Section titled “When declarative is not enough”Ninety-nine percent of React is declarative: change state, describe the UI, let React sync the DOM. But a few real problems need you to step outside that model — reach into the DOM, force timing, or render somewhere the component tree does not naturally allow. React provides deliberate escape hatches for these. The skill is knowing they exist and using them sparingly.
Portals: render outside the parent DOM
Section titled “Portals: render outside the parent DOM”A portal renders children into a different part of the DOM tree, while keeping them in the React tree where you wrote them. This is the standard solution for modals, tooltips, and dropdowns — UI that must escape a parent’s overflow: hidden or z-index stacking context.
import { createPortal } from "react-dom";
function Modal({ children }) { // Rendered into document.body, but still a React child of wherever <Modal/> is used. return createPortal( <div className="overlay">{children}</div>, document.body, );}flowchart TB react["React tree: App → Page → Modal → content"] dom["DOM tree: body → app-root ... body → overlay (portal target)"] react -->|logically a child of Page| dom note["Events still bubble through the React tree, not the DOM location"]
The key subtlety: a portal changes where the DOM node lives, not where it sits in the React tree. Context still flows in, and events still bubble up through the React parent — so a click inside a portaled modal still reaches handlers on its logical React ancestors.
Imperative refs: calling into a child or the DOM
Section titled “Imperative refs: calling into a child or the DOM”Most of the time data flows down through props. But sometimes a parent needs to imperatively trigger something on a child or DOM node — focus an input, play a video, scroll to an element. A ref gives you that handle.
function SearchForm() { const inputRef = useRef(null); return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> </> );}To expose a custom imperative API from a component (rather than the raw DOM node), use useImperativeHandle to define exactly which methods a parent’s ref can call. Prefer props/state for anything that can be declarative; reach for an imperative ref only for genuinely imperative actions (focus, scroll, media, measurement).
flushSync: opt out of batching
Section titled “flushSync: opt out of batching”React batches state updates and applies the DOM changes asynchronously for performance. Occasionally you need the DOM updated synchronously — for example, to read the new layout immediately (measure a just-added element, then scroll to it). flushSync forces a state update and its DOM commit to happen right away.
import { flushSync } from "react-dom";
flushSync(() => setItems([...items, newItem])); // DOM is updated by the time this returnslistRef.current.scrollTop = listRef.current.scrollHeight; // now safe to read/scrollflushSync defeats batching and hurts performance, so use it only when you truly must read or act on the DOM before the browser paints.