useRef Explained: Handling DOM Elements in React
Learn to Access and Manage DOM Elements in React Using useRef

Introduction
In React's declarative world, we typically let React handle DOM updates. However, there are times when you need to directly interact with DOM elements—focusing an input field, measuring an element's dimensions, or integrating with third-party libraries. This is where useRef becomes essential.
In this guide, we'll explore how to use useRef to access DOM nodes, starting from the basics and building up to production-ready patterns.
What is useRef?
useRef is a React Hook that creates a mutable reference object that persists across re-renders. Unlike state, updating a ref doesn't trigger a component re-render, making it perfect for storing values that shouldn't affect the visual output.
Basic Syntax
const myRef = useRef(initialValue);
The ref object has a single property called current that holds the actual value. When you attach a ref to a DOM element, React automatically sets ref.current to that DOM node.
Your First Ref: Focusing an Input
Let's start with the most common use case—focusing an input field when a component mounts.
Basic Example
import { useRef, useEffect } from 'react';
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input when component mounts
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
type="text"
placeholder="Start typing..."
/>
);
}
How it works:
We create a ref with
useRef(null)(null is the initial value)We attach the ref to the input using the
refattributeReact sets
inputRef.currentto the actual DOM elementIn
useEffect, we callinputRef.current.focus()to focus the input
Production-Grade Example: Advanced Search Component
Here's a real-world example that demonstrates multiple ref patterns you'll use in production applications.
import { useRef, useEffect, useState } from 'react';
function AdvancedSearchBar({ onSearch, autoFocus = false }) {
const [query, setQuery] = useState('');
const [isSearching, setIsSearching] = useState(false);
const inputRef = useRef(null);
const searchTimeoutRef = useRef(null);
// Auto-focus on mount if specified
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, []);
const handleInputChange = (e) => {
const value = e.target.value;
setQuery(value);
// Clear existing timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// Debounce search
setIsSearching(true);
searchTimeoutRef.current = setTimeout(() => {
onSearch(value);
setIsSearching(false);
}, 500);
};
const handleClear = () => {
setQuery('');
onSearch('');
// Focus input after clearing
inputRef.current?.focus();
};
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
handleClear();
}
};
return (
<div className="search-container">
<input
ref={inputRef}
type="text"
value={query}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Search..."
aria-label="Search input"
aria-busy={isSearching}
/>
{query && (
<button
onClick={handleClear}
aria-label="Clear search"
type="button"
>
Clear
</button>
)}
{isSearching && <span>Searching...</span>}
</div>
);
}
Key Patterns Demonstrated
1. Conditional Focus
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
Always check if ref.current exists before accessing it. This prevents errors during initial render or if the component unmounts.
2. Multiple Refs We use two refs: one for the DOM node (inputRef) and one for storing the timeout ID (searchTimeoutRef). Refs aren't just for DOM nodes—they're perfect for any mutable value that shouldn't cause re-renders.
3. Optional Chaining
inputRef.current?.focus();
Using ?. ensures we don't throw an error if the ref is null.
Common DOM Operations with useRef
Measuring Elements
function ImageGallery() {
const containerRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (containerRef.current) {
const { width, height } = containerRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div ref={containerRef}>
<p>Container size: {dimensions.width}x{dimensions.height}</p>
</div>
);
}
Scrolling to Elements
function ChatWindow({ messages }) {
const bottomRef = useRef(null);
useEffect(() => {
// Scroll to bottom when new messages arrive
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div className="chat-messages">
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
<div ref={bottomRef} />
</div>
);
}
Playing/Pausing Media
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
return (
<div>
<video ref={videoRef} src={src} />
<button onClick={togglePlay}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
}
Best Practices
1. Always Check for Null
// Good
if (inputRef.current) {
inputRef.current.focus();
}
// Better
inputRef.current?.focus();
// Bad - can throw error
inputRef.current.focus();
2. Don't Overuse Refs
Use refs only when you need to step outside React's declarative paradigm. For most UI updates, state and props are the right choice.
// Bad - using ref for something state should handle
const countRef = useRef(0);
countRef.current += 1; // Won't trigger re-render
// Good - use state for values that affect rendering
const [count, setCount] = useState(0);
setCount(c => c + 1); // Triggers re-render
3. Clean Up Side Effects
When using refs with timers, event listeners, or subscriptions, always clean them up.
useEffect(() => {
const element = elementRef.current;
const handleClick = () => console.log('clicked');
element?.addEventListener('click', handleClick);
return () => {
element?.removeEventListener('click', handleClick);
};
}, []);
4. Use TypeScript for Type Safety
const inputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const divRef = useRef<HTMLDivElement>(null);
Common Pitfalls to Avoid
Pitfall 1: Accessing Refs During Render
// Bad - don't access ref.current during render
function MyComponent() {
const ref = useRef(null);
console.log(ref.current); // Will be null on first render
return <div ref={ref}>Content</div>;
}
// Good - access in useEffect or event handlers
function MyComponent() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // Now it's safe
}, []);
return <div ref={ref}>Content</div>;
}
Pitfall 2: Forgetting Dependencies
// Bad - missing dependency
useEffect(() => {
inputRef.current?.focus();
}, []); // If inputRef comes from props, this is wrong
// Good - include all dependencies
useEffect(() => {
inputRef.current?.focus();
}, [inputRef]);
Conclusion
The useRef hook is a powerful tool for accessing DOM nodes in React. Remember these key points:
Use
useRefwhen you need direct DOM accessAlways check if
ref.currentexists before using itAccess refs in
useEffector event handlers, not during renderClean up any side effects that use refs
Prefer state for values that should trigger re-renders
With these patterns, you're ready to handle DOM manipulation confidently in your React applications.
Further Reading:






