Refs and DOM
Working with Refs in React
Refs provide a way to access DOM elements and component instances directly in React. They're useful for managing focus, triggering animations, or integrating with third-party libraries.
useRef Hook
Basic useRef Usage
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="Click button to focus"
/>
<button onClick={focusInput}>
Focus Input
</button>
</div>
);
}
Storing Mutable Values
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
const startTimer = () => {
if (!isRunning) {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
}
};
const stopTimer = () => {
if (isRunning) {
setIsRunning(false);
clearInterval(intervalRef.current);
}
};
const resetTimer = () => {
setSeconds(0);
setIsRunning(false);
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
useEffect(() => {
// Cleanup on unmount
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<h2>Timer: {seconds} seconds</h2>
<button onClick={startTimer} disabled={isRunning}>
Start
</button>
<button onClick={stopTimer} disabled={!isRunning}>
Stop
</button>
<button onClick={resetTimer}>
Reset
</button>
</div>
);
}
DOM Manipulation with Refs
Scrolling and Focus Management
function ScrollToElement() {
const topRef = useRef(null);
const middleRef = useRef(null);
const bottomRef = useRef(null);
const scrollToSection = (ref) => {
ref.current?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
};
return (
<div>
<nav style={{ position: 'fixed', top: 0, background: 'white' }}>
<button onClick={() => scrollToSection(topRef)}>
Go to Top
</button>
<button onClick={() => scrollToSection(middleRef)}>
Go to Middle
</button>
<button onClick={() => scrollToSection(bottomRef)}>
Go to Bottom
</button>
</nav>
<div style={{ marginTop: '60px' }}>
<div ref={topRef} style={{ height: '100vh', background: '#f0f8ff' }}>
<h2>Top Section</h2>
<p>This is the top of the page</p>
</div>
<div ref={middleRef} style={{ height: '100vh', background: '#f0fff0' }}>
<h2>Middle Section</h2>
<p>This is the middle of the page</p>
</div>
<div ref={bottomRef} style={{ height: '100vh', background: '#fff0f0' }}>
<h2>Bottom Section</h2>
<p>This is the bottom of the page</p>
</div>
</div>
</div>
);
}
Canvas Integration
function CanvasDrawing() {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const [lastPos, setLastPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Set canvas size
canvas.width = 800;
canvas.height = 400;
// Set drawing styles
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 2;
ctx.strokeStyle = '#000';
}, []);
const getMousePos = (e) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
};
const startDrawing = (e) => {
setIsDrawing(true);
const pos = getMousePos(e);
setLastPos(pos);
};
const draw = (e) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const currentPos = getMousePos(e);
ctx.beginPath();
ctx.moveTo(lastPos.x, lastPos.y);
ctx.lineTo(currentPos.x, currentPos.y);
ctx.stroke();
setLastPos(currentPos);
};
const stopDrawing = () => {
setIsDrawing(false);
};
const clearCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
return (
<div>
<h3>Canvas Drawing</h3>
<canvas
ref={canvasRef}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
style={{
border: '1px solid #ccc',
cursor: 'crosshair'
}}
/>
<br />
<button onClick={clearCanvas}>Clear Canvas</button>
</div>
);
}
forwardRef
Forwarding Refs to Custom Components
// Custom input component that forwards ref
const CustomInput = forwardRef(function CustomInput(props, ref) {
const { label, error, ...inputProps } = props;
return (
<div className="form-field">
{label && <label>{label}</label>}
<input
ref={ref}
className={error ? 'input-error' : 'input-normal'}
{...inputProps}
/>
{error && <span className="error-text">{error}</span>}
</div>
);
});
// Using the forwarded ref
function Form() {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const focusEmail = () => {
emailRef.current?.focus();
};
const focusPassword = () => {
passwordRef.current?.focus();
};
return (
<form>
<CustomInput
ref={emailRef}
label="Email"
type="email"
placeholder="Enter your email"
/>
<CustomInput
ref={passwordRef}
label="Password"
type="password"
placeholder="Enter your password"
/>
<div>
<button type="button" onClick={focusEmail}>
Focus Email
</button>
<button type="button" onClick={focusPassword}>
Focus Password
</button>
</div>
</form>
);
}
Complex Component with forwardRef
const Modal = forwardRef(function Modal({
isOpen,
onClose,
title,
children
}, ref) {
const modalRef = useRef(null);
// Combine external ref with internal ref
useImperativeHandle(ref, () => ({
focus: () => {
modalRef.current?.focus();
},
close: onClose,
getElement: () => modalRef.current
}));
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
ref={modalRef}
className="modal-content"
onClick={e => e.stopPropagation()}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Close modal">
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
});
// Using the modal with ref
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
const modalRef = useRef(null);
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
const focusModal = () => {
modalRef.current?.focus();
};
return (
<div>
<button onClick={openModal}>Open Modal</button>
<button onClick={focusModal}>Focus Modal</button>
<Modal
ref={modalRef}
isOpen={isModalOpen}
onClose={closeModal}
title="Example Modal"
>
<p>This is modal content</p>
<button onClick={closeModal}>Close from inside</button>
</Modal>
</div>
);
}
useImperativeHandle
Exposing Custom API
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play();
setIsPlaying(true);
},
pause: () => {
videoRef.current?.pause();
setIsPlaying(false);
},
stop: () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
setIsPlaying(false);
setCurrentTime(0);
}
},
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
setCurrentTime(time);
}
},
setVolume: (volume) => {
if (videoRef.current) {
videoRef.current.volume = Math.max(0, Math.min(1, volume));
}
},
isPlaying: () => isPlaying,
getCurrentTime: () => currentTime,
getDuration: () => duration
}));
const handleTimeUpdate = () => {
if (videoRef.current) {
setCurrentTime(videoRef.current.currentTime);
}
};
const handleLoadedMetadata = () => {
if (videoRef.current) {
setDuration(videoRef.current.duration);
}
};
return (
<video
ref={videoRef}
src={src}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
controls
style={{ width: '100%', maxWidth: '500px' }}
/>
);
});
// Using the video player
function VideoApp() {
const playerRef = useRef(null);
const handlePlay = () => {
playerRef.current?.play();
};
const handlePause = () => {
playerRef.current?.pause();
};
const handleStop = () => {
playerRef.current?.stop();
};
const handleSeek = (seconds) => {
playerRef.current?.seek(seconds);
};
const getPlayerInfo = () => {
if (playerRef.current) {
const isPlaying = playerRef.current.isPlaying();
const currentTime = playerRef.current.getCurrentTime();
const duration = playerRef.current.getDuration();
alert(`Playing: ${isPlaying}, Time: ${currentTime.toFixed(1)}/${duration.toFixed(1)}s`);
}
};
return (
<div>
<VideoPlayer
ref={playerRef}
src="/sample-video.mp4"
/>
<div style={{ marginTop: '10px' }}>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleStop}>Stop</button>
<button onClick={() => handleSeek(30)}>Seek to 30s</button>
<button onClick={getPlayerInfo}>Get Info</button>
</div>
</div>
);
}
Refs with Lists and Dynamic Elements
Managing Multiple Refs
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React refs', completed: false },
{ id: 2, text: 'Build a todo app', completed: false }
]);
const [editingId, setEditingId] = useState(null);
const inputRefs = useRef(new Map());
const addTodo = () => {
const newTodo = {
id: Date.now(),
text: 'New todo',
completed: false
};
setTodos(prev => [...prev, newTodo]);
setEditingId(newTodo.id);
};
const startEditing = (id) => {
setEditingId(id);
// Focus input after state update
setTimeout(() => {
const input = inputRefs.current.get(id);
if (input) {
input.focus();
input.select();
}
}, 0);
};
const saveEdit = (id, newText) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
setEditingId(null);
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
inputRefs.current.delete(id);
};
const setInputRef = (id, element) => {
if (element) {
inputRefs.current.set(id, element);
} else {
inputRefs.current.delete(id);
}
};
return (
<div>
<h3>Todo List with Refs</h3>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ margin: '10px 0' }}>
{editingId === todo.id ? (
<EditableInput
initialValue={todo.text}
onSave={(text) => saveEdit(todo.id, text)}
onCancel={() => setEditingId(null)}
ref={(el) => setInputRef(todo.id, el)}
/>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span>{todo.text}</span>
<button onClick={() => startEditing(todo.id)}>Edit</button>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
)}
</li>
))}
</ul>
</div>
);
}
const EditableInput = forwardRef(function EditableInput({
initialValue,
onSave,
onCancel
}, ref) {
const [value, setValue] = useState(initialValue);
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
onSave(value);
} else if (e.key === 'Escape') {
onCancel();
}
};
return (
<div style={{ display: 'flex', gap: '5px' }}>
<input
ref={ref}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => onSave(value)}
/>
<button onClick={() => onSave(value)}>Save</button>
<button onClick={onCancel}>Cancel</button>
</div>
);
});
Third-Party Library Integration
Integrating with DOM Libraries
function ChartComponent({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (!chartRef.current) return;
// Initialize chart library (example with Chart.js)
const ctx = chartRef.current.getContext('2d');
chartInstance.current = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'Sample Data',
data: data.values,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
return () => {
// Cleanup
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
}, []);
useEffect(() => {
// Update chart when data changes
if (chartInstance.current) {
chartInstance.current.data.labels = data.labels;
chartInstance.current.data.datasets[0].data = data.values;
chartInstance.current.update();
}
}, [data]);
return (
<div style={{ height: '400px', width: '100%' }}>
<canvas ref={chartRef} />
</div>
);
}
Common Patterns and Best Practices
Ref Callback Pattern
function AutoFocusInput({ shouldFocus }) {
const [inputElement, setInputElement] = useState(null);
// Ref callback function
const inputRef = useCallback((node) => {
if (node !== null) {
setInputElement(node);
if (shouldFocus) {
node.focus();
}
}
}, [shouldFocus]);
return (
<input
ref={inputRef}
placeholder="This input auto-focuses"
/>
);
}
Measuring Elements
function MeasuredComponent() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const elementRef = useRef(null);
useEffect(() => {
const updateDimensions = () => {
if (elementRef.current) {
const { offsetWidth, offsetHeight } = elementRef.current;
setDimensions({ width: offsetWidth, height: offsetHeight });
}
};
updateDimensions();
const resizeObserver = new ResizeObserver(updateDimensions);
if (elementRef.current) {
resizeObserver.observe(elementRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<div>
<div
ref={elementRef}
style={{
width: '50%',
height: '200px',
background: 'lightblue',
resize: 'both',
overflow: 'auto',
border: '1px solid #ccc'
}}
>
Resize me!
</div>
<p>Dimensions: {dimensions.width} x {dimensions.height}</p>
</div>
);
}
Best Practices
-
Use Refs Sparingly
- Prefer React's declarative patterns
- Only use refs when necessary
-
Common Valid Use Cases
- Managing focus, text selection, media playback
- Triggering animations
- Integrating with third-party DOM libraries
-
Avoid Refs For
- Anything that can be done declaratively
- Accessing child component data
- Triggering re-renders
-
Cleanup
- Clear intervals and timeouts stored in refs
- Remove event listeners
- Destroy third-party library instances
-
Ref Forwarding
- Use forwardRef for reusable components
- Use useImperativeHandle to expose only necessary methods
-
Performance
- Don't overuse refs in lists
- Use ref callbacks for dynamic elements
- Consider using keys for list items
Refs are a powerful escape hatch for accessing the DOM directly, but should be used judiciously in React applications!