Introduction
In Virendana Ui, I was debugging a form component that wouldn't focus the right input after validation errors. My React app was perfectly declarative everywhere else, but I needed to grab that DOM element and tell it exactly what to do. That's when I learned that "manipulating the DOM manually" isn't React heresy.
"Wait, are we allowed to do that in React? I thought we weren't supposed to touch the DOM directly 🤔"
You can — and sometimes you must. React just wants you to do it intentionally, not accidentally. That's where refs come in. They're your sanctioned way to step outside React's declarative world when you genuinely need to.
What Refs Actually Do (Beyond the Textbook Definition)
A ref gives you a direct line to a DOM element or component instance. Think of it as React's way of saying "here's the emergency exit from my declarative world."
In practice, I reach for refs when React's state-driven updates aren't enough:
- Focusing inputs after form submission
- Scrolling to specific sections (smooth scroll libraries need DOM access)
- Measuring element dimensions for responsive layouts
- Controlling video/audio playback
- Integrating with third-party libraries that expect DOM elements
- Clearing form state without triggering expensive re-renders
The key insight: refs let you command the DOM instead of just describing it. Most of the time, describing is better. But when you need precision control, refs are your tool.
Creating DOM Refs That Actually Work
import { useRef } from "react";
const SearchForm = () => {
const searchInputRef = useRef(null);
const handleSearchSubmit = () => {
// After processing, keep focus on input for better UX
if (searchInputRef.current) {
searchInputRef.current.focus();
}
};
return (
<div>
<input
ref={searchInputRef}
placeholder="Search products..."
type="search"
/>
<button onClick={handleSearchSubmit}>Search</button>
</div>
);
};
👇 The Pattern That Works:
useRef(null)creates the ref container — always initialize with nullref={searchInputRef}connects it to the actual DOM elementrefName.currentgives you the HTMLInputElement- You can call any DOM method:
.focus(),.blur(),.scrollIntoView()
Pro tip: I always add error checking in production: refName.current?.focus() — because refs can be null during initial render or if the component unmounts.
Real-World Ref Patterns (From Actual Projects)
1. ✅ Auto-Focus After Error States
const LoginForm = () => {
const emailInputRef = useRef();
const [error, setError] = useState('');
useEffect(() => {
// Focus first invalid field when errors appear
if (error && emailInputRef.current) {
emailInputRef.current.focus();
emailInputRef.current.select(); // Select all text for easy re-typing
}
}, [error]);
return (
<form>
<input
ref={emailInputRef}
type="email"
placeholder="Email"
aria-invalid={error ? 'true' : 'false'}
/>
{error && <span role="alert">{error}</span>}
</form>
);
};
2. ✅ Smart Scrolling in Single-Page Apps
import { useRef } from 'react';
const DocumentationPage = () => {
const sectionRefs = useRef({});
const scrollToSection = (sectionId) => {
const element = sectionRefs.current[sectionId];
if (element) {
// Custom scroll with offset for fixed headers
const y = element.offsetTop - 80; // Account for sticky nav
window.scrollTo({ top: y, behavior: 'smooth' });
}
};
const setSectionRef = (sectionId) => (el) => {
if (el) sectionRefs.current[sectionId] = el;
};
return (
<div>
<nav>
<button onClick={() => scrollToSection('api')}>API Docs</button>
<button onClick={() => scrollToSection('examples')}>Examples</button>
</nav>
<section ref={setSectionRef('api')}>API Documentation</section>
<section ref={setSectionRef('examples')}>Code Examples</section>
</div>
);
};
3. ✅ Media Control (Beyond Basic Play/Pause)
import { useRef, useState } from 'react';
const VideoPlayer = ({ src, onTimeUpdate }) => {
const videoRef = useRef();
const [isPlaying, setIsPlaying] = useState(false);
const togglePlayback = async () => {
const video = videoRef.current;
if (!video) return;
try {
if (video.paused) {
await video.play();
setIsPlaying(true);
} else {
video.pause();
setIsPlaying(false);
}
} catch (error) {
console.error('Playback failed:', error);
}
};
const skipToTime = (seconds) => {
if (videoRef.current) {
videoRef.current.currentTime = seconds;
}
};
return (
<div>
<video
ref={videoRef}
src={src}
onTimeUpdate={() => onTimeUpdate?.(videoRef.current?.currentTime)}
onEnded={() => setIsPlaying(false)}
width="300"
/>
<button onClick={togglePlayback}>
{isPlaying ? '⏸️' : '▶️'}
</button>
<button onClick={() => skipToTime(30)}>Skip 30s</button>
</div>
);
};
⚖️ My Decision Framework for Using Refs
✅ I use refs when:
- I need to trigger browser APIs (focus, scroll, measure, play/pause)
- I'm integrating third-party libraries that expect DOM elements
- I need to imperatively control something that can't be described declaratively
- Performance matters and I want to avoid unnecessary re-renders
❌ I avoid refs when:
- The value needs to trigger UI updates (use
useStateinstead) - I'm trying to shortcut React's rendering process
- I could achieve the same result with props, state, or CSS
Rule of thumb: If it affects what the user sees, it's probably state. If it affects how the user interacts with existing elements, it might be a ref.
🧼 Ref Patterns That Prevent Production Bugs
| Pattern | Why It Matters |
|---|---|
Always null-check: ref.current?. |
Prevents crashes during initial render or after unmount |
| Use refs in effects, not render | DOM elements may not exist during initial render |
| Don't rely on refs for conditional logic | Ref changes don't trigger re-renders |
| Clean up if needed | Remove event listeners or cancel operations in useEffect cleanup |
| One ref per element | Easier to debug and reason about |
🚫 Ref Mistakes That Cost Me Hours of Debugging
| Mistake | What Happens | The Fix |
|---|---|---|
Accessing ref.current in render |
Intermittent crashes, especially on first load | Move to useEffect or event handlers |
| Expecting UI updates from ref changes | Changes happen but UI doesn't reflect them | Use useState for data that affects rendering |
Forgetting the ref prop |
Ref stays null forever |
Double-check JSX: ref={myRef} |
| Using refs on conditional elements | Ref becomes null when element disappears | Always check existence before accessing |
✅ DOM Refs Quick Reference
| Concept | What You Need to Know |
|---|---|
| Purpose | Direct DOM access for imperative operations |
| Hook | useRef(null) |
| Access pattern | ref.current?.methodName() |
| Best use cases | Focus management, scrolling, media control, measurements |
| Re-render behavior | ❌ Changing .current never causes re-renders |
Questions I Get About DOM Refs
1. Can I modify element styles directly with refs?
Yes, but I prefer CSS classes. Use ref.current.style.property = "value" only for dynamic values that can't be predetermined in CSS.
2. Do refs cause re-renders like state?
Never. Refs are React's "silent storage" — perfect for values you need to keep but that don't affect what renders.
3. Should I use refs for form input values?
Only for uncontrolled inputs where you read the value on submit. For real-time validation or UI updates, stick with useState.
4. Can one ref point to multiple elements?
Not directly. For multiple elements, use an object or Map: const refs = useRef({}), then refs.current[id] = element.
5. When should I use refs instead of state?
When you need to interact with the DOM directly or store data that doesn't influence what gets rendered. Think "does this change what the user sees?" If no, consider a ref.