Refs, DOM access and portals

React · Practical · 14 min

Focusing inputs, measuring elements, holding timer ids and integrating non-React code all need refs.

A ref is a box whose .current you can read and write without causing a re-render:

const intervalRef = useRef(null);
intervalRef.current = setInterval(tick, 1000);

Use refs for values that the rendered output doesn't depend on: timer ids, the previous value of something, an instance of a third-party library. If changing the value should update the screen, it must be state. Don't read or write ref.current during rendering (except lazy one-time initialisation); do it in handlers and effects, otherwise rendering becomes impure.

The most common use is accessing DOM nodes. Pass a ref object to a built-in element's ref attribute and React sets .current to the DOM node after it mounts (and back to null when it unmounts):

const inputRef = useRef(null);
<input ref={inputRef} />
// in a handler: inputRef.current.focus();

In React 19, ref is an ordinary prop for function components: a child can accept { ref } and forward it to an inner element, so forwardRef is no longer needed. You can also pass a ref callback, ref={node => ...}, which may now return a cleanup function, useful for attaching observers per node.

Use DOM refs for things React doesn't model: focus, scroll position, measuring, media playback, integrating a chart library. Don't use them to change content React manages (adding children, changing text); React will overwrite or crash on it.

A related escape hatch is the portal. createPortal(children, domNode) from react-dom renders children into a different DOM node, like document.body, while they stay in the same place in the React tree, so context and event bubbling still work. It's how modals, tooltips and toasts escape overflow: hidden containers.

Example

import { useRef, useState } from "react";
import { createPortal } from "react-dom";

// React 19: ref is a regular prop, no forwardRef needed
function TextField({ label, ref }) {
  return <label>{label} <input ref={ref} /></label>;
}

export default function Search() {
  const inputRef = useRef(null);
  const renders = useRef(0);          // mutable, never triggers a render
  const [open, setOpen] = useState(false);

  return (
    <>
      <TextField label="Search" ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <button onClick={() => { renders.current++; setOpen(true); }}>Help</button>
      {open &&
        createPortal(
          <div className="modal">
            <p>Type and press Enter.</p>
            <button onClick={() => setOpen(false)}>Close</button>
          </div>,
          document.body          // rendered here, but still a child in React's tree
        )}
    </>
  );
}

Remember

  • useRef holds a mutable .current that persists across renders without triggering them
  • Pass a ref to ref={...} to get the DOM node; in React 19 ref is a normal prop for components
  • Don't read/write refs during render; use createPortal to render into another DOM node

Watch out for

  • Storing something in a ref that the UI displays, so the screen doesn't update when it changes.

Check yourself

  1. You store a click count in a ref and render {countRef.current}. Clicking increments countRef.current. What does the user see?
  2. What stays the same for content rendered with createPortal into document.body?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/manipulating-the-dom-with-refs

Builds on

Unlocks

Nothing builds on this yet.