useRef: DOM Access and Mutable Values

Understand how useRef creates a mutable container that survives re-renders without causing them, and use it to read DOM nodes directly and to hold values like timer IDs that the UI doesn't need to reflect.

Most data in a React component should live in state, because state changes are what tell React to re-render. But not everything a component needs to remember belongs on screen — a timer ID, a previous scroll position, or a direct handle to a DOM node are all things a component needs to keep track of without ever triggering a re-render when they change. useRef is the hook built for exactly that gap: it hands back a plain object with a single mutable property, current, that persists for the lifetime of the component instance.

What useRef Actually Returns

Calling useRef(initialValue) returns an object shaped like { current: initialValue }, and React guarantees that the same object identity is returned on every re-render of that component instance. You can read ref.current and write to it as many times as you like, and none of those writes cause a re-render — React simply doesn't know or care that the value changed, because refs sit outside the render-and-commit cycle entirely.

This is the key difference from useState: setting state schedules a re-render so the new value can appear in the UI, while mutating a ref's current property is a side, silent operation. That makes refs the wrong tool for anything the user needs to see, but the right tool for anything a component needs to remember internally between renders without involving React's reconciliation at all.

Because the ref object itself never changes identity across renders, it's safe to read or write ref.current inside event handlers, timeouts, or effects without worrying about stale closures the way you would with a captured state variable — the object reference is stable, only the current property underneath it changes.

Attaching Refs to DOM Elements

The most common use of useRef is wiring it directly to a rendered DOM element via the element's ref attribute. When you write <input ref={inputRef} />, React sets inputRef.current to the actual HTMLInputElement after that element is mounted into the DOM, and sets it back to null when the element unmounts. This gives you an escape hatch to call imperative DOM APIs — focus(), scrollIntoView(), play(), measuring dimensions — that have no declarative React equivalent.

Typing matters here: useRef<HTMLInputElement>(null) tells TypeScript that ref.current is either an HTMLInputElement or null, which forces you to null-check before calling methods on it (inputRef.current?.focus()). This isn't boilerplate for its own sake — the ref genuinely is null before the first render commits and after the element unmounts, so the check reflects real runtime behavior.

A DOM ref is only guaranteed to be attached after the component has committed to the DOM, which means you read it inside an event handler, inside a useEffect callback, or after some later render — never directly in the body of the component function during render, since the DOM node doesn't exist yet at that point.

Mutable Values That Aren't the DOM

useRef is just as useful for values that have nothing to do with the DOM. A classic example is storing the ID returned by setInterval or setTimeout so it can be cleared later — that ID needs to persist across renders (so a later event handler can find it) but changing it should never cause a re-render, which makes it a textbook ref use case rather than a state use case.

Other common non-DOM uses include storing the previous value of a prop or state variable for comparison, holding an instance of something created once (like a class-based library object or a WebSocket connection), or tracking whether a component is still mounted before setting state in an async callback. In every case the underlying pattern is the same: this is bookkeeping data the component needs, not data the UI renders.

Ref vs State: Choosing the Right Tool

The decision test is simple: if changing the value should cause something on screen to update, it belongs in state. If the value is invisible to the rendered output and only matters for the component's internal logic — a timer handle, a flag, a cached calculation input — it belongs in a ref.

Using state for something that should be a ref causes unnecessary re-renders and can even create bugs, like a re-render loop if the state setter is called on every keystroke of a timer tick that nobody needs to see. Using a ref for something that should be state produces the opposite bug: the underlying value changes correctly, but the screen never updates because React was never told to re-render.

A useful mental shortcut: state answers 'what should this render as,' while a ref answers 'what does this component need to remember.' Most non-trivial components use both, and the mistake to avoid is reaching for one when the other is a better fit for the specific value in question.

Creating and attaching a DOM ref

inputRef.current starts out null and is only set to the real DOM node after the element has mounted. The optional chaining (?.) guards against calling focus() before the ref is attached or after the element has unmounted.

const inputRef = useRef<HTMLInputElement>(null);

function focusInput() {
  inputRef.current?.focus();
}

return <input ref={inputRef} type="text" />;

Code Examples

The effect runs once after the component's first render, by which point inputRef.current already points at the real <input> DOM node, so calling focus() moves the cursor into it automatically when the component appears. Expected result: The input is focused automatically as soon as the component mounts, with no user click required.

The interval ID is stored in a ref rather than state because the ID itself is never displayed — only the seconds count is. Storing it in state would cause an extra, pointless re-render every time start() ran, and reading a stale closed-over ID from state inside stop() would be more error-prone than reading the always-current ref. The cleanup effect (useEffect(() => stop, [])) guarantees that if the component unmounts while the stopwatch is running, the interval is cleared instead of leaking and continuing to fire after the component is gone.

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" placeholder="Search..." />;
}
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef<number | null>(null);

  function start() {
    if (intervalRef.current !== null) return; // already running
    intervalRef.current = window.setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  }

  function stop() {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }

  useEffect(() => stop, []); // clear any running interval on unmount

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

Ref vs state across a render

Mutating a ref never triggers the left-hand path — the screen only reflects the ref's latest value the next time something else causes a render.

State change:  setState -> React schedules render -> render -> commit -> screen updates
Ref change:    ref.current = x -> nothing scheduled -> screen unchanged until next render for another reason

Important Rules

Never read or write ref.current during the render itself — only inside event handlers, effects, or other callbacks that run after commit.

Type DOM refs with the specific element type and null, e.g. useRef<HTMLInputElement>(null), and null-check before calling DOM methods.

Use a ref for values the UI doesn't need to display; use state for anything the render output depends on.

Clear timers, subscriptions, or connections stored in a ref inside a cleanup function so they don't outlive the component.

Where this shows up

Form libraries use DOM refs to focus the first invalid field after a failed validation. Accessibility-conscious modals use refs to move focus into the dialog on open and back to the trigger element on close. Charting and video libraries that manage their own canvas or <video> element internally are handed a ref to a container div so they can mount their own imperative rendering inside React's tree. Analytics code frequently uses a ref as an 'isMounted' guard so an async response arriving after unmount doesn't try to update state on a gone component.

function Panel() { const countRef = useRef(0); return <button onClick={() => countRef.current++}>{countRef.current}</button>; } function Panel() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }

The ref version silently increments countRef.current on every click, but because mutating a ref never schedules a render, the number shown on the button never actually updates. Any value the UI displays must live in state, not a ref.

[concept] Why doesn't updating ref.current cause a component to re-render? A. Because refs can only be attached to DOM elements B. Because React intentionally excludes ref mutations from its render-scheduling mechanism C. Because ref.current is read-only after the first render D. Because refs are recreated on every render, discarding the update

B. Because React intentionally excludes ref mutations from its render-scheduling mechanism — React only schedules a re-render in response to state updates (or a parent re-rendering). Mutating ref.current is a plain object mutation that React's reconciler never observes, so it never triggers work.

[predict-output] const inputRef = useRef<HTMLInputElement>(null); console.log(inputRef.current) is logged during the component's very first render, before any effect runs. What does it log? A. The HTMLInputElement instance B. undefined C. null D. An error is thrown

C. null — During the initial render the JSX hasn't been committed to the DOM yet, so the ref still holds its initial value, null. It's only set to the real element after the commit phase.

[concept] Which value is the best candidate for storing in a ref rather than state? A. The text currently typed into a controlled input B. Whether a modal dialog is open C. The ID returned by setInterval so it can later be cleared D. The list of items currently displayed after filtering

C. The ID returned by setInterval so it can later be cleared — A setInterval ID is pure bookkeeping — the UI never displays it, and changing it shouldn't trigger a re-render. The other three options all directly affect what's rendered and belong in state.

Build a component with a text input and a 'Clear and focus' button. Clicking the button should clear the input's value directly via the DOM node and return focus to it, without using state to hold the input's value.

function ClearableInput() { const inputRef = useRef<HTMLInputElement>(null); function handleClear() { if (inputRef.current) { inputRef.current.value = ""; inputRef.current.focus(); } } return ( <div> <input ref={inputRef} type="text" /> <button onClick={handleClear}>Clear and focus</button> </div> ); }

You only need one ref. Set inputRef.current.value = "" and then call inputRef.current.focus() inside the button's onClick handler.