Skip to content

Modern React 9: Refs and DOM escape hatches

This is part 9 of the Modern React development series.

Refs hold mutable values that React does not use for rendering. They are the right tool for focus, measurement, imperative browser APIs, timers, and values that need to survive renders without causing a new render.

Concept

useRef returns a stable object with a mutable current property. Updating current does not re-render the component. When attached to JSX with ref, React fills current with the corresponding DOM node after commit.

Terms

  • Ref: A stable object whose current property can hold a mutable value.
  • DOM: Document Object Model, the browser’s object representation of the rendered page.
  • Escape hatch: A React API for cases where declarative rendering is not the whole job.
  • Commit: The phase where React applies rendered changes to the host environment, such as the browser DOM.

Mental model

Think of a ref as a side pocket. It can hold a DOM handle or mutable note, but React does not look in that pocket to decide what the UI should show.

How it is used

Use refs to focus inputs, scroll nodes into view, measure element sizes, store timer IDs, remember previous values for Effects, and integrate with imperative browser or third-party APIs.

How to use it

  1. Create a ref with useRef(initialValue).
  2. Attach it to a DOM element with the ref prop when you need a DOM handle.
  3. Read or write ref.current inside event handlers or Effects.
  4. Use state instead when changing the value should update the screen.

Example: Focus an input

import { useRef } from "react";
export function FocusNameButton() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} aria-label="Name" />
<button type="button" onClick={() => inputRef.current?.focus()}>
Focus name
</button>
</>
);
}
React output

The click handler uses a DOM method. The current focus target is not render state, so a ref is the right container.

Example: Store a timer ID

import { useRef } from "react";
export function SaveStatus() {
const timeoutRef = useRef<number | null>(null);
function scheduleSavedMessage() {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = null;
}, 1200);
}
return <button onClick={scheduleSavedMessage}>Save draft</button>;
}
React output

The timer ID must survive renders, but showing the timer ID is not part of the UI.

Details to watch

  • Render reads: Do not use refs as hidden render state. If the UI depends on a value, use state.
  • Timing: DOM refs are set after React commits the element.
  • Nullability: DOM refs can be null before mount and after unmount.
  • Imperative APIs: Keep imperative calls contained in handlers, Effects, or small adapter components.

Series navigation

References