Accessing a DOM Node

Pass a ref object to the ref attribute of a built-in element and React 7,897 puts that element's DOM node into current when it commits, then sets current back to null when the node is removed. Initialize the ref with null, because that is what it holds before the first commit.

Focusing a text input through a refHTML
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{"imports":{"react":"https://esm.sh/react@19.3.0?dev",
"react-dom/client":"https://esm.sh/react-dom@19.3.0/client?dev"}}</script>
<div id="root"></div><script type="text/babel" data-type="module">
  import React, { useRef, useState } from 'react';
  import { createRoot } from 'react-dom/client';
  function TextInputWithFocusButton() {
    const inputEl = useRef(null);                  // null until React commits
    const [active, setActive] = useState('nothing');
    const focusIt = () => { inputEl.current.focus(); setActive(document.activeElement.id); };
    return <div style={{font: '16px system-ui'}}>
      <input id="name" ref={inputEl} placeholder="your name" />{' '}
      <button onClick={focusIt}>Focus the input</button>
      <p>document.activeElement is #<b>{active}</b></p></div>;
  }
  createRoot(document.getElementById('root')).render(<TextInputWithFocusButton />);
  setTimeout(() => document.querySelector('button').click(), 400);
</script>
Browser output of Listing 2.41
Browser output of 41

The screenshot is taken after a scripted click, so the input carries the focus ring and the readout names the focused node. Nothing here reads inputEl.current during render — the only safe places are event handlers and effects, because during render the node for this render does not exist yet.

Two limits are worth knowing before you hit them. First, ref on a component (<MyInput ref={r} />) is not automatic: the component has to accept it and pass it down, which is Refs as Props. Second, React still owns the node. Reading from it — measuring, focusing, scrolling, calling play() — is safe; adding or removing children by hand is not, because React's next update diffs against a tree that no longer matches the DOM. And since updates are batched, the DOM has not changed yet on the line after setItems(...): to measure or scroll immediately afterwards, wrap the update in flushSync from react-dom, sparingly.