React: Understanding useRef
While useState is the most commonly used hook in React, useRef is arguably the most versatile. Beginners often learn that useRef is used to select DOM elements, but it serves a second, equally important purpose.
What is
useRef? How is it different fromuseState?
1. Use Case A: Accessing the DOM directly
In React, the Virtual DOM abstracts away direct HTML manipulation. You shouldn't be using document.getElementById to change styles or focus inputs. Instead, you use useRef to create a reference and attach it to a JSX element.
React guarantees that the .current property of the ref will point to the physical DOM node once the component mounts.
import { useRef } from 'react';
function SearchInput() {
const inputRef = useRef(null);
const focusInput = () => {
// We access the raw HTML input element and call the native .focus() method
inputRef.current.focus();
};
return (
<>
{/* Attach the ref to the element */}
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={focusInput}>Focus Search</button>
</>
);
}Common uses for DOM Refs:
- Managing focus, text selection, or media playback (
video.play()). - Integrating with third-party, non-React DOM libraries (like D3.js or Chart.js).
- Measuring element dimensions (using
getBoundingClientRect()).
2. Use Case B: Mutable Values without Re-rendering
This is the use case interviewers really want you to know.
useState stores a value, and when you update it, React re-renders the component.
useRef also stores a value (inside its .current property), but when you update it, React DOES NOT re-render the component.
Furthermore, unlike normal local variables which get wiped and re-created every time a component renders, a ref's value persists across re-renders.
Example: Tracking a Timer ID
If you start a setInterval, you need a place to store its ID so you can clear it later.
- If you store it in a normal variable (
let timerId;), it will reset to undefined on the next render. - If you store it in
useState, callingsetTimerIdwill trigger a completely unnecessary re-render of your UI.
useRef is the perfect vault for this data.
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
// Create a mutable ref to store the timer ID
const timerRef = useRef(null);
const start = () => {
// Only start if one isn't already running
if (timerRef.current !== null) return;
// Store the ID in the ref. This mutation does NOT cause a re-render!
timerRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
timerRef.current = null;
};
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}Senior-Level Interview Answer
The
useRefhook serves two primary architectural functions in React. First, it provides an escape hatch to the underlying physical DOM, allowing developers to natively focus elements, measure nodes, or bridge integrations with imperative third-party libraries by mapping the element to the ref's.currentproperty. Second, it acts as a persistent, mutable data vault across render cycles. UnlikeuseState, mutating a ref's.currentproperty operates entirely outside of React's render pipeline, meaning it avoids triggering reconciliation and re-renders. This makesuseRefthe optimal tool for storing infrastructure data that the visual UI doesn't strictly depend on, such as timeout/interval IDs, previous state comparisons, or observer instances.
Common Interview Mistakes
❌ Rendering the ref's value in JSX
A fundamental rule is that refs should not be rendered in your UI. Because React doesn't track mutations to refs, if you write <p>{myRef.current}</p>, the text will not update when myRef.current changes. Only state triggers UI updates.
❌ Misunderstanding the .current property
Candidates often try to mutate the ref object directly: myRef = "new value". This will throw an error or fail to persist. A ref is an object shaped like { current: initialValue }. You must always read and write via the .current property: myRef.current = "new value".
