useRef vs useState
useRef vs useState
Both useRef and useState allow you to store data that persists across component renders. However, they are used for fundamentally different purposes.
The Golden Rule
- Use
useStatefor values that should trigger a re-render when they change (e.g., text in an input, a toggle switch, fetched data that is displayed). - Use
useReffor values that should NOT trigger a re-render when they change (e.g., timer IDs, DOM nodes, keeping track of the previous state).
Comparison
| Feature | useState | useRef |
|---|---|---|
| Updating the value | Triggers a component re-render. | Does not trigger a re-render. |
| Mutability | Immutable. You must use the state setter function to update it. | Mutable. You can modify ref.current directly. |
| Reading the value | You can read it anywhere in your component. | You should only read/write it in event handlers or effects, not during rendering. |
| Primary Use Case | Data that affects the visual UI. | Storing DOM nodes or keeping persistent background data. |
Example: The Stopwatch
To illustrate the difference, consider a Stopwatch component.
We need to track two things:
timeElapsed: This needs to update the UI every 10 milliseconds, so it must be State.intervalId: We need to store the ID returned bysetIntervalso we can clear it later. Changing this ID does not change the UI, so it must be a Ref.
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// We use a ref to store the interval ID. If we used state for this,
// setting the ID would cause an unnecessary re-render!
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
// clearInterval expects the ID, so we store it in .current
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
// We read .current to clear the interval
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</>
);
}
Why not just use regular variables?
You might wonder why we can’t just use a normal let variable outside or inside the component instead of useRef.
let timerId = null; // Defined outside
function Component() {
// ...
}
If you define it outside, that variable is shared across all instances of the component. If you render <Stopwatch /> twice on the same page, clicking “Start” on one will overwrite the timerId of the other!
function Component() {
let timerId = null; // Defined inside
// ...
}
If you define it inside, that variable is completely recreated and reset to null every single time the component re-renders!
useRef solves this: it is localized to the specific component instance, and it survives re-renders.
Interview Questions
Q: What is the absolute primary difference between useRef and useState?
A: Mutating a state variable inherently triggers a component re-render so the UI can update. Mutating a ref’s .current property does NOT trigger a re-render. Refs are an “escape hatch” to store background mutable data (like interval IDs) that shouldn’t affect the visual output.