When working with DOM elements in React, you have two main approaches: using document.querySelector()
(the traditional JavaScript way) or useRef
(the React way). Here’s when and how to use each, along with key differences:
1. document.querySelector()
in React
import { useEffect } from 'react';
function Component() {
useEffect(() => {
const button = document.querySelector('#my-button');
button.addEventListener('click', () => {
console.log('Button clicked!');
});
return () => button.removeEventListener('click'); // Cleanup
}, []);
return <button id="my-button">Click Me</button>;
}
When to Use It:
- Legacy Integration (e.g., third-party libraries that need raw DOM access)
- One-Time DOM Query (e.g., initializing a non-React library like
Chart.js
) - Global Selectors (e.g.,
document.body
)
Problems:
- ❌ Breaks React’s Virtual DOM (direct DOM manipulation can cause conflicts)
- ❌ No React State Awareness (changes won’t trigger re-renders)
- ❌ Unreliable in Strict Mode (double-renders can break selectors)
2. useRef
(The React Way)
import { useRef } from 'react';
function Component() {
const buttonRef = useRef(null);
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.addEventListener('click', () => {
console.log('Button clicked!');
});
}
return () => {
if (buttonRef.current) {
buttonRef.current.removeEventListener('click');
}
};
}, []);
return <button ref={buttonRef}>Click Me</button>;
}
When to Use It:
- ✅ Managed DOM Access (works with React’s lifecycle)
- ✅ Dynamic Elements (refs update with re-renders)
- ✅ Performance (avoids DOM re-scans on every render)
Key Benefits:
- 🚀 React-Compatible: Survives re-renders without stale references
- 🧹 Automatic Cleanup: Easier to integrate with
useEffect
cleanup - 📌 Component-Scoped: Avoids global DOM pollution
3. Key Differences
Feature | document.querySelector() | useRef |
---|---|---|
React Integration | ❌ Bypasses Virtual DOM | ✅ Fully integrated |
Re-Render Safety | ❌ May break on updates | ✅ Stable across re-renders |
Scope | Global DOM | Component instance only |
Performance | Slower (re-scans DOM) | Faster (direct reference) |
Use Case | Legacy/third-party libs | Modern React apps |
4. Best Practice
- Prefer
useRef
for most React projects (safer, more performant) - Only use
document.querySelector
when: - Integrating non-React libraries
- Accessing elements outside your component (e.g.,
document.body
) - Debugging in dev tools (temporary use)
Example Where querySelector
is Acceptable:
useEffect(() => {
// One-time initialization of a jQuery plugin
const $element = document.querySelector('.third-party-widget');
$(element).pluginInit();
}, []);
Example Where useRef
is Better:
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // More reliable than querySelector
};
return <input ref={inputRef} />;
5. Advanced Pattern: Forwarding Refs
For reusable components:
const Button = React.forwardRef((props, ref) => (
<button ref={ref}>{props.children}</button>
));
// Parent component
function Parent() {
const buttonRef = useRef(null);
return <Button ref={buttonRef}>Click</Button>;
}
Final Verdict
- 95% of the time: Use
useRef
(React-optimized, cleaner code) - 5% edge cases: Use
document.querySelector
(escape hatch for special needs)