Learn how to use useRef to access DOM elements directly, store mutable values without triggering re-renders, and understand when to use refs versus state in your React applications.
In React, the standard way to interact with the DOM is through state and props — you describe what the UI should look like, and React handles the DOM updates. But sometimes you need to step outside this declarative model and interact with a DOM element directly.
Common scenarios:
For these cases, React provides the useRef hook. A ref (short for reference) is a way to hold a reference to a value that persists across renders but does not trigger a re-render when it changes.
const myRef = React.useRef(initialValue);useRef returns an object with a single property: current. The object itself is stable (same object every render), but you can freely read and write myRef.current.
const myRef = React.useRef(42);
console.log(myRef.current); // 42
myRef.current = 99;
console.log(myRef.current); // 99 — no re-render triggered!This is fundamentally different from useState: changing a ref's .current value does not cause the component to re-render.
The most common use of useRef is to get a reference to a DOM element. You do this by passing the ref to an element's ref attribute:
function TextInput() {
const inputRef = React.useRef(null);
function handleClick() {
// Access the actual DOM element
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Type here..." />
<button onClick={handleClick}>Focus the input</button>
</div>
);
}Here is what happens step by step:
React.useRef(null) creates a ref object: { current: null }ref={inputRef} attribute tells React: "After this <input> element is created in the DOM, set inputRef.current to that DOM node."inputRef.current is the actual <input> DOM element, so calling .focus() works just like vanilla JavaScript.inputRef.current back to null.function MeasuredBox() {
const boxRef = React.useRef(null);
const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });
React.useEffect(() => {
if (boxRef.current) {
const rect = boxRef.current.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
}, []);
return (
<div>
<div ref={boxRef} style={{ width: '200px', height: '100px', background: '#eee' }}>
Measured box
</div>
<p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
</div>
);
}Since the ref is null during the first render (the DOM element does not exist yet), we use useEffect to read the dimensions after the element has been painted.
Beyond DOM access, useRef is useful for storing any mutable value that should persist between renders but should not trigger a re-render when changed.
function Counter() {
const [count, setCount] = React.useState(0);
const prevCountRef = React.useRef(0);
React.useEffect(() => {
prevCountRef.current = count;
});
return (
<p>
Current: {count}, Previous: {prevCountRef.current}
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</p>
);
}Refs are perfect for holding timer IDs that need to be cleared later:
function Stopwatch() {
const [seconds, setSeconds] = React.useState(0);
const intervalRef = React.useRef(null);
function start() {
if (intervalRef.current) return; // already running
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Cleanup on unmount
React.useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}If we used useState for the interval ID, every start/stop would trigger an unnecessary re-render. Since we never need to display the interval ID, a ref is the right choice.
Use useState | Use useRef |
|---|---|
| Value is displayed in the UI | Value is not displayed in the UI |
| Changing it should trigger a re-render | Changing it should not trigger a re-render |
| Examples: counter, form input, toggle | Examples: timer ID, previous value, DOM node |
By default, function components do not expose their internal DOM nodes. If a parent wants to focus an input inside a child component, you need to forward the ref.
React.forwardRef creates a component that can receive a ref from its parent and forward it to a DOM element:
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
return (
<input
ref={ref}
type="text"
style={{ border: '2px solid blue', padding: '8px' }}
{...props}
/>
);
});
// Parent can now ref the inner <input>
function Form() {
const inputRef = React.useRef(null);
return (
<div>
<FancyInput ref={inputRef} placeholder="Fancy!" />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</div>
);
}Sometimes you want to expose only specific methods to the parent, rather than the entire DOM node. useImperativeHandle lets you customize the ref value:
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
const inputRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = '';
},
}));
return <input ref={inputRef} {...props} />;
});
// Parent sees only focus() and clear(), not the raw DOM node
function Form() {
const inputRef = React.useRef(null);
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
<button onClick={() => inputRef.current.clear()}>Clear</button>
</div>
);
}This pattern follows the principle of least privilege — the parent gets only the API it needs, not direct DOM access.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function App() {
const inputRef = React.useRef(null);
const renderCount = React.useRef(0);
const [text, setText] = React.useState("");
renderCount.current += 1;
return (
<div>
<h2>useRef Demo</h2>
<input
ref={inputRef}
value={text}
onChange={e => setText(e.target.value)}
placeholder="Type something..."
/>
<button onClick={() => inputRef.current.focus()}>
Focus Input
</button>
<button onClick={() => { inputRef.current.value = ""; setText(""); }}>
Clear
</button>
<p>You typed: {text}</p>
<p>Render count: {renderCount.current}</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>What happens when you change the .current property of a ref created with useRef?