Skip to content

Refs, Portals & Escape Hatches

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.

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"]
A portal splits DOM location from tree position

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).

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 returns
listRef.current.scrollTop = listRef.current.scrollHeight; // now safe to read/scroll

flushSync defeats batching and hurts performance, so use it only when you truly must read or act on the DOM before the browser paints.

What does `createPortal` do?
A click inside a portaled modal — where do its events bubble?
When should you reach for an imperative ref instead of props/state?
What is `flushSync` for?