useRef(initialValue) returns an object with a single mutable property, .current, initialized to initialValue — unlike state, updating a ref's .current value doesn't cause the component to re-render, and unlike a plain local variable, a ref's value persists identically across every re-render of the component rather than being reset each time. It has two extremely common uses: holding a direct reference to a DOM element by passing the ref object to a JSX element's ref attribute, and storing any mutable value that needs to persist across renders but genuinely shouldn't trigger a re-render when it changes, like a timer ID or a previous value for comparison.
1Understanding useRef
useRef(initialValue) returns an object with a single mutable property, .current, initialized to initialValue — unlike state, updating a ref's .current value doesn't cause the component to re-render, and unlike a plain local variable, a ref's value persists identically across every re-render of the component rather than being reset each time. It has two extremely common uses: holding a direct reference to a DOM element by passing the ref object to a JSX element's ref attribute, and storing any mutable value that needs to persist across renders but genuinely shouldn't trigger a re-render when it changes, like a timer ID or a previous value for comparison.
Reading or writing ref.current never causes a re-render — if you need the UI to actually update in response to a value changing, that value belongs in state via useState, not in a ref.
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</>
);
}2Practical Example
Here is a real-world application of useRef showing how it is used in production React code.
import { useRef, useEffect } from 'react';
function RenderCounter() {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log('Rendered', renderCount.current, 'times');
});
return <p>Check the console.</p>;
}3Best Practices
Follow these guidelines when working with useRef:
1. Use a ref specifically to access a DOM element directly, like calling .focus() on an input, by passing the ref object to that element's ref attribute
2. Store values that need to persist across renders but shouldn't trigger a re-render when they change, like a timer ID or previous-value tracker, in a ref rather than state
3. Avoid reading or writing ref.current during rendering itself, since refs are meant for imperative access outside the render/pure-function flow, typically inside event handlers or effects
Tip: Reading or writing ref.current never causes a re-render — if you need the UI to actually update in response to a value changing, that value belongs in state via useState, not in a ref.
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</>
);
}4The Previous Value Pattern
A common use of useRef is remembering a prop or state value from the previous render, to compare it against the current one. An effect updates the ref to the latest value after each render commits, so during render itself the ref still holds whatever was current on the render before — a lightweight way to detect that a value just changed.
function PriceTag({ price }) {
const prevPrice = useRef(price);
useEffect(() => { prevPrice.current = price; });
const isIncreasing = price > prevPrice.current;
return <span>{isIncreasing ? '↑' : '↓'} {price}</span>;
}5Refs vs. State: A Decision Framework
Reach for useState whenever a value's change should be reflected on screen — React needs to know about it to re-render. Reach for useRef whenever you need to remember something across renders that the UI never directly displays: a DOM node, a timer ID, a render counter, or a previous value for comparison.
// Visible in the UI -> useState
const [count, setCount] = useState(0);
// Invisible bookkeeping -> useRef
const timerId = useRef(null);