What is useRef?
useRef returns a mutable object that persists across renders. It has two main uses: (1) accessing DOM elements directly, and (2) storing mutable values that don't trigger re-renders when changed.
useRef vs useState:
- useState: Triggers re-render when updated
- useRef: Does NOT trigger re-render when updated
📚 Basic Syntax
import { useRef } from 'react';
function Component() {
const ref = useRef(initialValue);
// Access/update value
ref.current // Read value
ref.current = newValue // Update (no re-render!)
return <div>...</div>;
}
🎯 Use Case 1: DOM References
Focus Input on Mount:
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input when component mounts
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
type="text"
placeholder="Search..."
/>
);
}
Scroll to Element:
function ScrollExample() {
const topRef = useRef(null);
const bottomRef = useRef(null);
const scrollToTop = () => {
topRef.current.scrollIntoView({ behavior: 'smooth' });
};
const scrollToBottom = () => {
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<div ref={topRef}>Top of page</div>
{/* Long content */}
<div ref={bottomRef}>Bottom of page</div>
<button onClick={scrollToTop}>↑ Top</button>
<button onClick={scrollToBottom}>↓ Bottom</button>
</div>
);
}
Play/Pause Video:
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => {
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>
);
}
Measure Element Size:
function MeasureBox() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState({});
useEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={boxRef} style={{ width: '200px', height: '100px', background: 'blue' }}>
Box
</div>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
);
}
💾 Use Case 2: Storing Mutable Values
Previous Value:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Custom hook version
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
const prevCount = usePrevious(count);
Interval ID:
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current !== null) return;
setIsRunning(true);
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current === null) return;
clearInterval(intervalRef.current);
intervalRef.current = null;
setIsRunning(false);
};
const reset = () => {
stop();
setSeconds(0);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<h2>{seconds}s</h2>
<button onClick={start} disabled={isRunning}>Start</button>
<button onClick={stop} disabled={!isRunning}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Render Count:
function RenderCounter() {
const renderCount = useRef(0);
// Increment on every render (doesn't cause re-render!)
renderCount.current += 1;
return <div>Renders: {renderCount.current}</div>;
}
Latest Value in Callback:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const latestMessagesRef = useRef(messages);
// Keep ref in sync
useEffect(() => {
latestMessagesRef.current = messages;
}, [messages]);
useEffect(() => {
const ws = new WebSocket(`wss://api.com/room/${roomId}`);
ws.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
// Use ref to get latest messages without recreating listener
setMessages([...latestMessagesRef.current, newMessage]);
};
return () => ws.close();
}, [roomId]); // No need to include messages in dependencies!
return <div>{/* render messages */}</div>;
}
🎨 Complete Example: Click Outside
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
// Add listener when dropdown is open
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
return (
<div ref={dropdownRef} className="dropdown">
<button onClick={() => setIsOpen(!isOpen)}>
Menu {isOpen ? '▲' : '▼'}
</button>
{isOpen && (
<div className="dropdown-menu">
<a href="#">Profile</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</div>
)}
</div>
);
}
// Reusable custom hook
function useClickOutside(callback) {
const ref = useRef(null);
useEffect(() => {
function handleClick(event) {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [callback]);
return ref;
}
// Usage
function Modal({ onClose }) {
const modalRef = useClickOutside(onClose);
return (
<div ref={modalRef} className="modal">
Modal content
</div>
);
}
🔄 useRef with useEffect
// Debounce with useRef
function SearchBox() {
const [search, setSearch] = useState('');
const [results, setResults] = useState([]);
const timeoutRef = useRef(null);
useEffect(() => {
// Clear previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout
timeoutRef.current = setTimeout(() => {
if (search) {
fetch(`/api/search?q=${search}`)
.then(res => res.json())
.then(setResults);
}
}, 500);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [search]);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
{results.map(r => <div key={r.id}>{r.name}</div>)}
</div>
);
}
⚠️ Common Mistakes
1. Using Ref Value in Render
// ❌ Won't update UI when ref changes
function Bad() {
const ref = useRef(0);
return <div>{ref.current}</div>; // Won't re-render
}
// ✅ Use state if you need re-renders
function Good() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
2. Ref Not Set Yet
// ❌ Ref might be null
function Bad() {
const ref = useRef(null);
ref.current.focus(); // Error if ref.current is null!
}
// ✅ Check if ref is set
function Good() {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
}, []);
}
🎯 Key Takeaways
- useRef: Mutable value that persists across renders
- No re-render: Changing ref.current doesn't trigger re-render
- DOM access: Attach to elements with ref attribute
- Store values: Intervals, timeouts, previous values
- ref.current: Access/update the stored value
- Initialize: useRef(initialValue)
- Check null: Always check if DOM ref is set
- Cleanup: Clear intervals/timeouts in useEffect cleanup