Manipulating the DOM with Refs
React manages the DOM for you — you describe what the UI should look like and React makes it so. But occasionally you need to step outside React's model and talk to the DOM directly: focusing an input, measuring an element's dimensions, scrolling a container, or integrating with a third-party library that expects a real DOM node.
The ref attribute on a DOM element is the official escape hatch for these situations. Pair it with useRef and you have a direct reference to the underlying DOM node.
Attaching a Ref to a DOM Element
import { useRef } from 'react'
function Example() {
const inputRef = useRef(null)
// After the component mounts, inputRef.current === the <input> DOM node
return <input ref={inputRef} type="text" />
}The lifecycle is precise:
Before mount —
ref.currentisnullAfter mount —
ref.currentis the DOM nodeAfter unmount —
ref.currentis reset tonull
Auto-Focus on Mount
A common requirement: focus a search box or modal input the moment it appears. Use useEffect with an empty dependency array to run code after the first render:
import { useRef, useEffect } from 'react'
function SearchModal({ isOpen }) {
const inputRef = useRef(null)
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus()
}
}, [isOpen]) // re-run whenever the modal opens
if (!isOpen) return null
return (
<div role="dialog" aria-modal="true">
<input
ref={inputRef}
type="search"
placeholder="Search..."
autoComplete="off"
/>
</div>
)
}Scroll to Bottom — Chat Window
Chat applications need to scroll the message list to the bottom whenever a new message arrives. Refs make this simple:
import { useRef, useEffect } from 'react'
function ChatWindow({ messages }) {
const bottomRef = useRef(null)
useEffect(() => {
// Scroll the sentinel element into view after every message update
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div
style={{
height: 400,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: 16,
}}
>
{messages.map(msg => (
<div key={msg.id} className={msg.isOwn ? 'bubble-own' : 'bubble-other'}>
{msg.text}
</div>
))}
{/* Invisible sentinel at the very bottom */}
<div ref={bottomRef} />
</div>
)
}Measuring Element Dimensions
Sometimes you need to know how large an element is — to position a tooltip, build a custom resize observer, or compute a dynamic layout:
import { useRef, useState, useLayoutEffect } from 'react'
function MeasuredBox({ children }) {
const boxRef = useRef(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useLayoutEffect(() => {
if (!boxRef.current) return
const { width, height } = boxRef.current.getBoundingClientRect()
setDimensions({ width, height })
// Optionally observe future size changes:
const observer = new ResizeObserver(([entry]) => {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
})
})
observer.observe(boxRef.current)
return () => observer.disconnect()
}, [])
return (
<div ref={boxRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid' }}>
{children}
<small style={{ display: 'block', color: 'gray' }}>
{dimensions.width.toFixed(0)} × {dimensions.height.toFixed(0)} px
</small>
</div>
)
}Triggering a CSS Animation Imperatively
When you need a one-shot animation triggered by an event (e.g., shake on invalid input), you can add and remove a CSS class directly via the DOM:
import { useRef } from 'react'
// CSS (in your stylesheet):
// @keyframes shake {
// 0%, 100% { transform: translateX(0) }
// 20%, 60% { transform: translateX(-8px) }
// 40%, 80% { transform: translateX(8px) }
// }
// .shake { animation: shake 0.4s ease; }
function ValidatedInput({ onSubmit }) {
const inputRef = useRef(null)
function handleSubmit() {
const value = inputRef.current.value.trim()
if (!value) {
// Shake the input to signal invalid
const el = inputRef.current
el.classList.remove('shake')
// Force reflow so the animation restarts if already running
void el.offsetWidth
el.classList.add('shake')
el.addEventListener('animationend', () => el.classList.remove('shake'), { once: true })
return
}
onSubmit(value)
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Enter a value" />
<button onClick={handleSubmit}>Submit</button>
</div>
)
}The useEffect + Ref Pattern
Almost all imperative DOM work follows the same structure: create the ref, attach it to the element, then use useEffect to run the imperative code after React has committed the DOM:
function MapWidget({ lat, lng }) {
const containerRef = useRef(null)
const mapRef = useRef(null) // store the library instance
// Initialize the map once on mount
useEffect(() => {
if (!containerRef.current) return
mapRef.current = new ThirdPartyMap(containerRef.current, { lat, lng, zoom: 14 })
return () => mapRef.current?.destroy() // cleanup on unmount
}, [])
// Update the map when coordinates change
useEffect(() => {
mapRef.current?.setCenter({ lat, lng })
}, [lat, lng])
return <div ref={containerRef} style={{ width: '100%', height: 400 }} />
}Multiple Refs in the Same Component
function VideoPlayer({ src }) {
const videoRef = useRef(null)
const progressRef = useRef(null)
function handlePlay() { videoRef.current?.play() }
function handlePause() { videoRef.current?.pause() }
function handleTimeUpdate() {
const pct = (videoRef.current.currentTime / videoRef.current.duration) * 100
progressRef.current.style.width = pct + '%'
}
return (
<div>
<video ref={videoRef} src={src} onTimeUpdate={handleTimeUpdate} />
<div style={{ height: 4, background: '#eee' }}>
<div ref={progressRef} style={{ height: '100%', background: 'blue', width: '0%' }} />
</div>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
</div>
)
}Ref Callbacks for Dynamic Lists
When you need refs for a dynamic number of elements (you cannot call hooks in a loop), use a ref callback — a function that React calls with the DOM node when the element mounts:
import { useRef } from 'react'
function HighlightableList({ items }) {
const itemRefs = useRef(new Map())
function scrollToItem(id) {
itemRefs.current.get(id)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
return (
<>
<ul>
{items.map(item => (
<li
key={item.id}
ref={node => {
if (node) itemRefs.current.set(item.id, node)
else itemRefs.current.delete(item.id)
}}
>
{item.name}
</li>
))}
</ul>
<button onClick={() => scrollToItem(items[items.length - 1].id)}>
Scroll to last
</button>
</>
)
}