Purpose of the useRef hook in React
The Short Answer
useRef creates a mutable container that persists across renders without triggering re-renders when changed. It has two primary uses: holding a reference to a DOM element (for imperative operations like focusing an input) and storing mutable values that need to survive re-renders but shouldn't cause them (like timer IDs, previous values, or instance-level flags).
How It Works
useRef returns an object with a single property: current. This object is created once on the first render and the same object is returned on every subsequent render. You can read and write .current freely — React doesn't track changes to it, so mutations never trigger re-renders.
const myRef = useRef(initialValue);
// The ref object:
// { current: initialValue }
// Reading:
console.log(myRef.current);
// Writing (does NOT trigger re-render):
myRef.current = newValue;
// Key insight: myRef is the SAME object across all renders
// Only .current changes — the container itself is stable
Use Case 1: DOM References
The most common use of useRef is getting a reference to a DOM element for imperative operations — focusing an input, scrolling to an element, measuring dimensions, or integrating with third-party DOM libraries. You attach the ref to a JSX element via the ref attribute, and React sets .current to the DOM node after mounting.
function SearchBar() {
const inputRef = useRef<HTMLInputElement>(null);
// Focus the input when the component mounts
useEffect(() => {
inputRef.current?.focus();
}, []);
// Focus programmatically on button click
function handleClear() {
setQuery('');
inputRef.current?.focus(); // Imperative DOM operation
}
const [query, setQuery] = useState('');
return (
<div>
<input
ref={inputRef} // React sets inputRef.current = this DOM node
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={handleClear}>Clear</button>
</div>
);
}
Other DOM ref use cases include scrolling an element into view, measuring element dimensions with getBoundingClientRect(), integrating with canvas or video APIs, and attaching third-party libraries that need a DOM node (like chart libraries or map widgets).
function MessageList({ messages }: { messages: Message[] }) {
const bottomRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages.length]);
// Measure container height
function logHeight() {
const height = containerRef.current?.getBoundingClientRect().height;
console.log('Container height:', height);
}
return (
<div ref={containerRef} className="overflow-y-auto h-96">
{messages.map(msg => <MessageBubble key={msg.id} message={msg} />)}
<div ref={bottomRef} /> {/* Invisible scroll anchor */}
</div>
);
}
Use Case 2: Mutable Values That Don't Trigger Re-renders
The second major use case is storing values that need to persist across renders but shouldn't cause re-renders when they change. Think of it as an instance variable for function components. Common examples: timer IDs, previous prop values, flags to track whether something has happened, and counters for non-UI purposes.
function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start() {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setElapsed(prev => prev + 1);
}, 1000);
}
function stop() {
setIsRunning(false);
if (intervalRef.current) {
clearInterval(intervalRef.current); // Access the timer ID without re-render
intervalRef.current = null;
}
}
// Cleanup on unmount
useEffect(() => {
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
return (
<div>
<p>{elapsed}s</p>
<button onClick={isRunning ? stop : start}>
{isRunning ? 'Stop' : 'Start'}
</button>
</div>
);
}
If you stored the interval ID in state instead of a ref, clearing it would trigger an unnecessary re-render. The ref lets you store and access the ID without any rendering cost.
Use Case 3: Tracking Previous Values
A common pattern is using a ref to remember the previous value of a prop or state variable. Since refs persist across renders and don't trigger re-renders, you can update them in an effect to always hold the "last render's" value.
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value; // Update AFTER render — so during render it holds the old value
});
return ref.current; // Returns the value from the PREVIOUS render
}
// Usage
function PriceDisplay({ price }: { price: number }) {
const previousPrice = usePrevious(price);
const direction = previousPrice !== undefined
? price > previousPrice ? '📈' : price < previousPrice ? '📉' : ''
: '';
return <span>{direction} ${price}</span>;
}
useRef vs useState
| Aspect | useRef | useState |
|---|---|---|
| Triggers re-render on change? | No — silent mutation | Yes — schedules re-render |
| Value available immediately? | Yes — synchronous write/read | No — new value available next render |
| Persists across renders? | Yes | Yes |
| Use for UI? | No — changes won't show on screen | Yes — drives what's rendered |
| Use for | DOM refs, timer IDs, flags, previous values | Data that should be reflected in the UI |
The decision rule
If changing the value should update what the user sees → useState. If the value is needed internally but shouldn't cause a visual update → useRef. If you find yourself setting state just to store a value you never render, that's a ref.
Common Mistakes
Reading ref.current during render for display
If you read `ref.current` in your JSX to display a value, changes to it won't show up — React doesn't know the ref changed, so it doesn't re-render. The UI shows stale data.
✅If a value needs to be displayed, use useState. Refs are for values that don't affect the rendered output.
Writing to ref.current during render
Mutating a ref during the render phase (outside useEffect) can cause issues with concurrent features and Strict Mode. React may call your component multiple times during render.
✅Write to refs inside useEffect, event handlers, or callbacks — never during the render phase itself (except for lazy initialization patterns).
Why Interviewers Ask This
This question tests whether you understand the distinction between values that drive rendering (state) and values that exist outside the render cycle (refs). Interviewers want to see that you know both use cases (DOM access and mutable containers), understand why refs don't trigger re-renders, can articulate when to use a ref vs state, and know the patterns for common scenarios like timers, previous values, and imperative DOM operations. It reveals your depth of understanding of React's rendering model.
Quick Revision Cheat Sheet
Returns: A mutable object { current: value } that persists across renders
DOM refs: Attach to JSX elements for imperative operations (focus, scroll, measure)
Mutable storage: Timer IDs, flags, counters — anything that shouldn't trigger re-renders
Previous values: Update in useEffect to always hold last render's value
vs useState: Ref = silent, synchronous, no re-render. State = triggers re-render
Rule: Don't read refs during render for display — use state for visible data