useRef Beyond DOM Nodes: Mutable Values Without Re-renders

useRef returns a mutable container whose .current property you can change without triggering a re-render. DOM access is just one use case. The other — storing mutable values that should persist across renders but not drive the UI — is equally important.

June 7, 20263 min read1 / 2

useRef returns a plain object with a single property: .current. That object is created once and persists for the full lifetime of the component. You can read it, write to it, and it survives every re-render unchanged.

The critical difference from state: changing .current does not trigger a re-render.

TSX
const ref = useRef(0); ref.current = 42; // React has no idea this happened

This one property — that mutations are invisible to React — is what makes useRef useful in two distinct situations.

Use Case 1: Accessing DOM Elements Directly

React normally handles the DOM for you. But sometimes you need to call a native DOM method directly — focusing an input, measuring an element's dimensions, triggering video playback. For those cases, create a ref and attach it to the element:

TSX
const inputRef = useRef(null); <input type="text" ref={inputRef} />

Once attached, inputRef.current is the actual DOM node. Every native method and property on that element is available:

TSX
// Focus the input programmatically inputRef.current.focus(); // Read the current value without controlled state console.log(inputRef.current.value);

The ref gives you a direct line to the DOM element without going through state. No onChange, no controlled input pattern required — just imperative access when you specifically need it.

Mutating a Ref Does Not Re-render

This distinction matters:

TSX
// State — React re-renders on every call const [value, setValue] = useState(''); setValue('hello'); // triggers re-render // Ref — React never knows const valueRef = useRef(''); valueRef.current = 'hello'; // no re-render

If you added a useEffect with no dependency array and logged every re-render, setting state would produce new logs. Mutating a ref would produce none.

Refs are for values that need to persist and change, but that should not drive the UI.

Use Case 2: Storing Mutable Values Across Renders

Refs are not limited to DOM elements. Any mutable value that should survive re-renders without causing them belongs in a ref — timer IDs, interval handles, previous values, flags:

TSX
const timerRef = useRef(null); function startTimer() { timerRef.current = setInterval(() => doSomething(), 1000); } function stopTimer() { clearInterval(timerRef.current); }

The timer ID is stored between renders. Starting and stopping it causes no re-renders. The value is always available whenever you need it.

Tracking the Previous Value of State

A common pattern: knowing what a state value was before the last update.

TSX
const [count, setCount] = useState(0); const previousCount = useRef(0); useEffect(() => { previousCount.current = count; }, [count]); return ( <> <p>Current: {count}</p> <p>Previous: {previousCount.current}</p> <button onClick={() => setCount(prev => prev + 1)}>Increment</button> </> );

The effect runs after every count change and writes the current value into the ref. On the next render, previousCount.current holds what count was before. The ref update causes no extra render — just a value stored silently in the background.

useRef vs useState: The Decision

useStateuseRef
Triggers re-render when changedYesNo
Value visible in JSXYesOnly if explicitly rendered
Persists across rendersYesYes
Good forUI-driving valuesSide-channel data, DOM access

If changing the value should update the UI — use state. If changing the value should not — use a ref. That is the entire decision.

Practice what you just read.

useRef or useState?Build usePrevious
2 exercises