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
currentproperty 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
- Create a ref with
useRef(initialValue). - Attach it to a DOM element with the
refprop when you need a DOM handle. - Read or write
ref.currentinside event handlers or Effects. - 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> </> );}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>;}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
nullbefore mount and after unmount. - Imperative APIs: Keep imperative calls contained in handlers, Effects, or small adapter components.
Series navigation
- Previous: Part 8: Context without global soup
- Next: Part 10: Effects, synchronization, and cleanup
- Series index: Modern React development