Refs and when useRef beats useState
I used to reach for useState for everything. Here is why useRef is often the better tool for values that do not need to trigger a re-render.
For a long time I treated useState as the default tool for any value I needed to keep around in a component. State worked, so I kept using it. It took me a while to realise that every call to useState comes with a cost: a re-render. That cost is zero if the value never changes, but it adds up quickly when you are storing something that does not actually need to appear on screen.
useRef is React's way of giving you a mutable box that persists across renders without triggering an update. It is simple, but easy to misuse. This is what I have learned about when to use it, and when to leave it alone.
What useRef actually stores
A ref is an object with a single .current property. React gives you the same object on every render. If you mutate .current, React does not know about it and will not re-run your component.
import { useRef } from 'react';
function Counter() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(countRef.current);
};
return <button onClick={handleClick}>Clicked {0} times</button>;
}
Clicking the button increments the ref, but the UI stays stuck at zero because the component never re-renders. That is the point. The value is there when you need it, but it does not participate in React's rendering cycle.
The DOM access use case
The most common reason to reach for useRef is to grab a DOM node. You pass the ref to a JSX element, and React writes the node into .current after mounting.
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
This does not cause a re-render. The ref sits quietly, holds the node, and lets you interact with it imperatively when you need to.
Holding values that should not trigger renders
This is where I used to overuse state. Timers, intervals, and form values that are read later but never displayed are perfect for refs.
import { useRef, useEffect } from 'react';
function Stopwatch() {
const timerRef = useRef<number | null>(null);
const start = () => {
if (timerRef.current) return;
timerRef.current = window.setInterval(() => {
// update something externally, e.g. a canvas
}, 100);
};
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
useEffect(() => stop, []);
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
If timerRef were state, every setInterval call would force a re-render with the exact same timer ID. The UI would not change, but React would do the work anyway.
Remembering the previous value
There is no built-in "previous props" hook, but a ref makes it trivial to implement.
import { useRef, useEffect } from 'react';
function usePrevious<T>(value: T) {
const ref = useRef<T>(value);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
The ref stores the value from the last render. Because it is updated in useEffect, which runs after paint, reading ref.current during render gives you the previous value. This is cleaner than wrestling with state just to compare old and new data.
Avoiding stale closures in event handlers
One subtle case where refs shine is inside long-lived callbacks, such as event listeners or intervals. If you close over state, the callback sees the value from the render that created it. A ref always reads the latest .current.
import { useRef, useEffect } from 'react';
function MouseTracker() {
const posRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => {
posRef.current = { x: e.clientX, y: e.clientY };
};
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return <button onClick={() => console.log(posRef.current)}>Log position</button>;
}
If posRef were state, the event listener would need to be re-attached on every change, or you would need a more complex pattern. The ref removes that friction.
When not to use useRef
Refs are escape hatches, not replacements for state. If a value is rendered on screen and should update when it changes, use useState. If you find yourself manually calling forceUpdate or reading a ref just to trigger a render, you chose the wrong tool.
Mutating .current during render is also unsafe. If you need to derive a value during render, do it directly or use state. Changing a ref while React is calculating the next output can lead to confusing bugs.
Wrap-up
useRef is not a niche feature. It is a built-in way to keep mutable, non-rendering state in React. I now default to useState for anything the user sees, and useRef for everything else: DOM nodes, timers, previous values, and fresh data inside closures. The distinction keeps components lean and avoids unnecessary renders.
Newsletter
A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.