The useRef Hook
useRef creates a ref object for a function component.
const ref = useRef(initialValue);The returned object stays the same between renders.
{
current: initialValue
}You can read or write ref.current.
DOM Refs
The most common first use of useRef is storing a DOM element.
function LoginForm() {
const emailRef = useRef(null);
function focusEmail() {
emailRef.current?.focus();
}
return (
<form>
<input ref={emailRef} type="email" />
<button type="button" onClick={focusEmail}>
Focus email
</button>
</form>
);
}React fills emailRef.current after the input is mounted.
Mutable Values That Do Not Render
Refs can also store values that should not affect rendering.
function Stopwatch() {
const intervalIdRef = useRef(null);
const [seconds, setSeconds] = useState(0);
function start() {
if (intervalIdRef.current !== null) {
return;
}
intervalIdRef.current = setInterval(() => {
setSeconds((value) => value + 1);
}, 1000);
}
function stop() {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
return (
<>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}The timer ID is not displayed, so it belongs in a ref. The seconds value is displayed, so it belongs in state.
Ref vs State
Use state when:
- the value affects what is rendered
- changing the value should update the screen
- the value is part of the component's visible behavior
Use a ref when:
- the value needs to survive renders
- changing the value should not trigger a render
- the value represents an imperative handle, timer ID, DOM node, or previous value
Storing Previous Values
A ref can remember a previous prop or state value.
function PriceChange({ price }) {
const previousPriceRef = useRef(price);
useEffect(() => {
previousPriceRef.current = price;
}, [price]);
const previousPrice = previousPriceRef.current;
return (
<p>
Current: {price}, previous: {previousPrice}
</p>
);
}The effect runs after rendering, so during the next render the ref contains the previous value.
Do Not Read Layout During Render
This is a common mistake:
function BadMeasure() {
const boxRef = useRef(null);
const width = boxRef.current?.offsetWidth;
return <div ref={boxRef}>Width: {width}</div>;
}During render, the DOM may not exist yet. Even when it does, reading layout during render creates confusing timing.
Measure in an effect instead.
function MeasureBox() {
const boxRef = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
if (!boxRef.current) {
return;
}
setWidth(boxRef.current.offsetWidth);
}, []);
return <div ref={boxRef}>Width: {width}</div>;
}Updating a Ref Is Immediate
Unlike state updates, ref writes are synchronous.
valueRef.current = "saved";
console.log(valueRef.current); // "saved"That does not mean refs are better than state. It means refs are useful for mutable bookkeeping.
Common Mistakes
- Expecting
ref.current = valueto update the UI. - Using refs to avoid learning state or effects.
- Creating a ref inside a loop or condition.
- Reading DOM measurements during render.
- Forgetting to clear timers or subscriptions stored in refs.
A value should be stored in state instead of a ref when...
Recap
useRef gives you a stable object whose current value can change without re-rendering.
Use it for DOM access, timers, previous values, and integration details. Use state for visible UI data.
Practice
Create a component with a text input, a "Save draft" button, and a visible "Saved count".
Store the latest draft text in a ref, but store the saved count in state so the screen updates when the count changes.