Difficulty: Intermediate
useRef returns a mutable ref object whose .current property is initialized to the passed argument. The returned object persists for the full lifetime of the component. Unlike state, mutating a ref does not cause a re-render. This makes useRef ideal for two distinct purposes: accessing DOM elements and storing mutable values that don't affect the visual output.
The most common use of useRef is to access DOM elements. By passing a ref object as the ref attribute on a JSX element, React sets ref.current to the corresponding DOM node after the component mounts. This lets you imperatively focus inputs, measure element dimensions, scroll to positions, or integrate with third-party DOM libraries. The ref is set to null when the element unmounts.
The second important use is storing mutable instance variables - values that persist across renders but whose changes don't need to trigger re-renders. Common examples include timer IDs, previous prop/state values, flags for tracking mount status, and counters for tracking renders. Think of useRef as a box that can hold any mutable value, similar to instance variables on a class.
A key distinction between useRef and useState is that updating ref.current does not cause a re-render. This is both a feature and a source of bugs. If you need the UI to reflect a value, use state. If you need to track a value without causing re-renders (like a timer ID or a cancelled flag), use a ref. Mixing these up is a common mistake.
Refs are also useful for capturing the previous value of a prop or state variable. By updating the ref in a useEffect, you can compare the current value with the previous one. This pattern is common enough that many codebases extract it into a usePrevious custom hook.
function FocusInput() {
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log('inputRef.current:', inputRef.current ? 'HTMLInputElement' : 'null');
// In a real app: inputRef.current.focus();
}, []);
console.log('Rendering FocusInput');
// In real JSX: <input ref={inputRef} />
return null;
}
FocusInput();
useRef(null) creates a ref that will hold a DOM element. The ref is attached via the ref attribute in JSX. In an effect, you can access the DOM node through ref.current.
function RenderCounter() {
const renderCount = React.useRef(0);
// This runs on every render but does NOT cause re-render
renderCount.current += 1;
console.log('Render count:', renderCount.current);
console.log('Type of renderCount:', typeof renderCount);
console.log('Has .current:', 'current' in renderCount);
return null;
}
RenderCounter();
Mutating renderCount.current does not trigger a re-render. The ref persists across renders, making it perfect for tracking values without causing update cycles.
function RefVsState() {
const [stateVal, setStateVal] = React.useState(0);
const refVal = React.useRef(0);
// Updating ref does NOT cause re-render
refVal.current = 42;
console.log('refVal.current:', refVal.current);
console.log('stateVal:', stateVal);
// Updating state DOES cause re-render
// setStateVal(42) would trigger a re-render
console.log('Ref update triggers re-render: no');
console.log('State update triggers re-render: yes');
return null;
}
RefVsState();
This highlights the key difference: ref mutations are silent (no re-render), while state updates trigger re-renders. Choose based on whether the UI needs to reflect the value.
useRef, DOM refs, mutable refs, ref vs state, ref callback