useRef Beyond DOM Nodes: Mutable Values Without Re-renders
useRef returns a mutable container whose .current property you can change without triggering a re-render. DOM access is just one use case. The other — storing mutable values that should persist across renders but not drive the UI — is equally important.
useRef returns a plain object with a single property: .current. That object is created once and persists for the full lifetime of the component. You can read it, write to it, and it survives every re-render unchanged.
The critical difference from state: changing .current does not trigger a re-render.
const ref = useRef(0);
ref.current = 42; // React has no idea this happenedThis one property — that mutations are invisible to React — is what makes useRef useful in two distinct situations.
Use Case 1: Accessing DOM Elements Directly
React normally handles the DOM for you. But sometimes you need to call a native DOM method directly — focusing an input, measuring an element's dimensions, triggering video playback. For those cases, create a ref and attach it to the element:
const inputRef = useRef(null);
<input type="text" ref={inputRef} />Once attached, inputRef.current is the actual DOM node. Every native method and property on that element is available:
// Focus the input programmatically
inputRef.current.focus();
// Read the current value without controlled state
console.log(inputRef.current.value);The ref gives you a direct line to the DOM element without going through state. No onChange, no controlled input pattern required — just imperative access when you specifically need it.
Mutating a Ref Does Not Re-render
This distinction matters:
// State — React re-renders on every call
const [value, setValue] = useState('');
setValue('hello'); // triggers re-render
// Ref — React never knows
const valueRef = useRef('');
valueRef.current = 'hello'; // no re-renderIf you added a useEffect with no dependency array and logged every re-render, setting state would produce new logs. Mutating a ref would produce none.
Refs are for values that need to persist and change, but that should not drive the UI.
Use Case 2: Storing Mutable Values Across Renders
Refs are not limited to DOM elements. Any mutable value that should survive re-renders without causing them belongs in a ref — timer IDs, interval handles, previous values, flags:
const timerRef = useRef(null);
function startTimer() {
timerRef.current = setInterval(() => doSomething(), 1000);
}
function stopTimer() {
clearInterval(timerRef.current);
}The timer ID is stored between renders. Starting and stopping it causes no re-renders. The value is always available whenever you need it.
Tracking the Previous Value of State
A common pattern: knowing what a state value was before the last update.
const [count, setCount] = useState(0);
const previousCount = useRef(0);
useEffect(() => {
previousCount.current = count;
}, [count]);
return (
<>
<p>Current: {count}</p>
<p>Previous: {previousCount.current}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
</>
);The effect runs after every count change and writes the current value into the ref. On the next render, previousCount.current holds what count was before. The ref update causes no extra render — just a value stored silently in the background.
useRef vs useState: The Decision
useState | useRef | |
|---|---|---|
| Triggers re-render when changed | Yes | No |
| Value visible in JSX | Yes | Only if explicitly rendered |
| Persists across renders | Yes | Yes |
| Good for | UI-driving values | Side-channel data, DOM access |
If changing the value should update the UI — use state. If changing the value should not — use a ref. That is the entire decision.
Practice what you just read.
Keep reading