When should you use refs instead of state in React
Refs for values that need to persist across renders but **don't drive rendering**: DOM nodes, timer/interval ids, mutable counters, latest-value mirrors for stale-closure fixes, third-party instances. State for values that, when changed, must trigger a re-render. Mutating a ref doesn't re-render; setting state does. Don't read/write refs during render — only in effects and handlers.
Refs and state are both per-component memory across renders. The key difference: state changes trigger re-renders; ref mutations don't. Pick by whether the value should drive rendering.
Use refs when
1. Accessing DOM nodes
const inputRef = useRef(null);
const focus = () => inputRef.current?.focus();
return <input ref={inputRef} />;React doesn't expose DOM nodes any other way.
2. Storing timer / interval / observer ids
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(...);
return () => clearInterval(timerRef.current);
}, []);The id doesn't change rendering — just cleanup.
3. Mutable values that don't drive UI
const renderCount = useRef(0);
useEffect(() => { renderCount.current++; });You want the value across renders but you're not displaying it (or you display it via state separately).
4. Latest-value mirror for stale closures
const latestValue = useRef(value);
useEffect(() => { latestValue.current = value; });
const callback = useCallback(() => {
// always reads latest, even with empty deps
use(latestValue.current);
}, []);See [[why-does-setcountcount-1-inside-usecallback-capture-stale-state]] for the bug this fixes.
5. Third-party library instances
const mapRef = useRef(null);
useEffect(() => {
mapRef.current = new Mapbox.Map({...});
return () => mapRef.current.destroy();
}, []);The map instance lives outside React's tree but tied to component lifecycle.
6. Tracking previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current;
}7. Imperative animations / direct DOM writes
Per-frame updates via rAF — write to element.style.transform via refs, don't put position in state. See [[build-a-cursor-tracker]].
Use state when
The value, when changed, should cause the UI to update:
- Input value displayed in the UI.
- Toggle (modal open/closed).
- Fetched data being rendered.
- Anything visible.
The rule
If changing the value should re-render the component or its children, it's state. If the value is bookkeeping that doesn't drive what users see, it's a ref.
Don't do this
Mutating a ref to "trigger" a re-render
// BAD
const ref = useRef(0);
const inc = () => ref.current++; // UI never updatesRefs don't re-render. Use state.
Reading/writing refs during render
function Bad() {
const ref = useRef(0);
ref.current++; // BAD — side effect during render
return <p>{ref.current}</p>;
}React's render must be pure. Read/write refs in effects or handlers only. (Exception: lazy ref initialization on first render is okay.)
Putting DOM nodes in state
// BAD
const [node, setNode] = useState(null);
<div ref={setNode} /> // re-renders on mount with the nodeUse useRef unless you specifically need a re-render when the node attaches (rare; useCallback ref is the right escape hatch).
Forwarding refs
To attach a ref to a child component's DOM node, the child must use React.forwardRef:
const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);React 19 simplifies this — ref becomes a prop directly.
Interview framing
"Refs and state both persist across renders. The defining difference: setting state triggers a re-render; mutating a ref doesn't. Use refs for values that bookkeep but don't drive UI — DOM nodes, timer ids, mutable counters, latest-value mirrors for stale closures, third-party instances, animation per-frame values. Use state when changing the value should update what the user sees. The hard rule: don't read or write refs during render — only in effects and handlers — because React's render must be pure. And don't try to use a ref to 'trigger' a re-render; if you need that, you need state."
Follow-up questions
- •Why don't ref mutations trigger re-renders?
- •When would you reach for a 'latest value' ref?
- •Why is reading/writing refs during render bad?
- •What's a callback ref and when do you use it?
Common mistakes
- •Trying to use a ref to trigger re-render.
- •Mutating ref.current during render.
- •Storing displayed values in refs (UI gets stale).
- •Putting DOM nodes in state (unnecessary re-render).
Performance considerations
- •Refs are zero-cost across renders. Direct DOM writes via refs sidestep React's reconciliation — useful for per-frame animation.
Edge cases
- •Lazy ref initialization (`useRef(() => ...)` not supported; init in effect or check on first read).
- •Refs to functional components — need forwardRef.
- •Concurrent rendering — refs aren't tracked, so don't mutate them during render.
Real-world examples
- •react-window scroll handlers.
- •Mapbox / Three.js wrappers.
- •Form libraries that read inputs imperatively.