Refs and DOM Access
Refs: The Escape Hatch
React is declarative — you describe what the UI should look like, and React handles the DOM. But sometimes you need direct DOM access: focusing an input, measuring an element, integrating with a non-React library. Refs are React's escape hatch for these cases.
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Direct DOM API call
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}
Think of a ref as a sticky note attached to the side of your component. It holds one value (current). Changing the sticky note does not trigger a re-render — React does not even notice. State is like the component's official record that React watches. Refs are the side channel that React ignores. Use state for values that affect what the user sees. Use refs for values that do not.
useRef: Two Distinct Use Cases
1. DOM Element References
function VideoPlayer({ src }) {
const videoRef = useRef(null);
function handlePlay() {
videoRef.current.play(); // Imperative DOM API
}
function handlePause() {
videoRef.current.pause();
}
return (
<div>
<video ref={videoRef} src={src} />
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
When React mounts the <video> element, it sets videoRef.current to the DOM node. On unmount, it sets it back to null.
2. Mutable Instance Variables
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current) return; // Prevent double-start
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => {
return () => clearInterval(intervalRef.current); // Cleanup on unmount
}, []);
return (
<div>
<span>{seconds}s</span>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
The interval ID is stored in a ref because:
- It does not affect rendering (no visual change when the ID changes)
- It needs to persist across renders (state would work but triggers unnecessary re-renders)
- It needs to be mutable (stop needs to read the current ID set by start)
Ref vs State: The Decision
| Question | State | Ref |
|---|---|---|
| Does changing it affect what the user sees? | Yes | No |
| Does React need to re-render when it changes? | Yes | No |
| Is it used only in event handlers or effects? | Either | Usually |
| Do you need the value during render? | Yes | Avoid |
Never read or write ref.current during rendering (in the component body, outside effects and handlers). Refs are mutable, and reading mutable values during render makes the component's output unpredictable. React may call your render function multiple times (concurrent features), and each call could see a different ref value. Keep ref access in effects and event handlers.
forwardRef: Passing Refs to Child Components
Custom components do not accept ref directly. You need forwardRef:
const FancyInput = forwardRef(function FancyInput(props, ref) {
return (
<input
ref={ref}
className="fancy-input"
{...props}
/>
);
});
function Form() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Works — ref forwarded to <input>
}, []);
return <FancyInput ref={inputRef} placeholder="Name" />;
}
React 19 ref as a prop
React 19 simplifies ref forwarding. You no longer need forwardRef — ref is passed as a regular prop:
// React 19 — ref is just a prop
function FancyInput({ ref, ...props }) {
return <input ref={ref} className="fancy-input" {...props} />;
}
// Usage is the same:
<FancyInput ref={inputRef} />forwardRef still works for backwards compatibility, but new code should use the prop pattern.
Callback Refs: Dynamic DOM Measurement
When you need to run code the moment a DOM node is attached or detached, use a callback ref:
function MeasuredBox() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<div ref={measuredRef}>
<p>Content that determines height</p>
</div>
<p>Height: {height}px</p>
</div>
);
}
Callback refs fire when the DOM node is attached (argument is the node) and when detached (argument is null). Unlike useRef + useEffect, callback refs work immediately — even for conditionally rendered elements.
Production Scenario: Focus Management in a Modal
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Store the previously focused element
previousFocusRef.current = document.activeElement;
// Focus the close button inside the modal
closeButtonRef.current?.focus();
}
return () => {
// Restore focus when modal closes
previousFocusRef.current?.focus();
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay" role="dialog" aria-modal="true">
<div className="modal-content">
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Close modal"
>
X
</button>
{children}
</div>
</div>
);
}
| What developers do | What they should do |
|---|---|
| Reading ref.current during render to make rendering decisions Refs are mutable. Reading them during render makes output unpredictable, especially with concurrent features where render may be called multiple times. | Read refs only in effects and event handlers |
| Using ref when state is needed — component does not update when ref changes Changing ref.current is invisible to React. The component will not re-render, and the UI will show stale data. | If the value affects what is displayed, use state. Refs do not trigger re-renders. |
| Passing ref directly to a custom component: <MyInput ref=`{ref}` /> (pre React 19) Before React 19, ref was not a regular prop on function components. It was consumed by React and not forwarded. forwardRef explicitly passes it through. | Use forwardRef to wrap the component, or pass as a differently named prop |
| Using useRef to store previous state values without understanding closure timing During render, the ref still holds the previous value. It should be updated in useEffect so it is in sync after the render completes. | Update the ref in useEffect, not during render |
- 1useRef returns
{ current: value }— a mutable container that persists across renders without triggering re-renders - 2Two use cases: DOM element access and mutable instance variables (timer IDs, previous values)
- 3Never read or write ref.current during render — only in effects and event handlers
- 4forwardRef passes refs to child components (unnecessary in React 19 where ref is a regular prop)
- 5Callback refs fire on mount (with node) and unmount (with null) — use for dynamic measurement