useRef, useMemo & useCallback
useRef, useMemo & useCallback
These three hooks give you fine-grained control over DOM access and render performance. They are the tools you reach for when a component is rendering too often or you need direct DOM interaction.
useRef — Two Distinct Use Cases
Use case 1: DOM element access
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus() // focus the actual DOM input on mount
}, [])
return <input ref={inputRef} type="text" placeholder="Auto-focused" />
}Attaching a ref to a JSX element gives you direct access to the DOM node. React sets inputRef.current to the element after the first render.
Use case 2: Persistent values that do not trigger re-renders
function Timer() {
const [seconds, setSeconds] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
function start() {
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current)
}
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}Storing the interval ID in a ref (not state) means clearing it does not cause a re-render.
useMemo — Expensive Computations
function CourseStats({ courses }: { courses: Course[] }) {
// Without useMemo: runs on every render, even when courses hasn't changed
// With useMemo: only recomputes when courses changes
const stats = useMemo(() => ({
total: courses.length,
totalXp: courses.reduce((sum, c) => sum + c.xp, 0),
byTrack: courses.reduce((acc, c) => {
acc[c.track] = (acc[c.track] || 0) + 1
return acc
}, {} as Record<string, number>),
masters: courses.filter(c => c.level === 'Masters').length,
}), [courses])
return <StatsDisplay stats={stats} />
}The rule: if a computation takes noticeable time AND the component re-renders often for unrelated reasons, wrap it in useMemo. Do not apply it everywhere — it has overhead of its own.
useCallback — Stable Function References
Every render creates new function instances. If you pass a function as a prop to a memoized child, the child re-renders on every parent render — even if the function "looks the same."
function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
// This function is recreated on every render — SearchButton always re-renders
const handleSearch = () => onSearch(query)
// useCallback: same reference until query or onSearch changes
const handleSearch = useCallback(() => {
onSearch(query)
}, [query, onSearch])
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SearchButton onClick={handleSearch} />
</div>
)
}
// memo: only re-renders when props change by reference
const SearchButton = React.memo(({ onClick }: { onClick: () => void }) => (
<button onClick={onClick}>Search</button>
))useCallback is most useful when: 1) you pass callbacks to memoized children, or 2) a callback is a dependency of another hook (useEffect, useMemo).
When to Optimize
The golden rule of React performance: measure first, optimize second. Use the React DevTools Profiler to record renders and see which components re-render and why before adding memoization.
Premature optimization with useMemo and useCallback everywhere adds complexity and can actually hurt performance (the comparison overhead on every render). Apply them surgically to confirmed bottlenecks.