useRef, useMemo, useCallback - When They Matter and When They Don't
A practical deep dive into useRef, useMemo, and useCallback - when they matter, when they don't, and the interview questions built around them.
Understanding these three hooks deeply separates developers who write working React from those who write performant, correct React. They're also the most commonly misunderstood topic in senior interviews.
The One-Sentence Version
useRef- a box that holds a value across renders without causing themuseMemo- remembers the result of a calculationuseCallback- remembers a function
That's it. Everything else is knowing when they matter and when they don't.
useRef - More Than Just DOM Access
Most developers know useRef for grabbing DOM nodes:
const inputRef = useRef(null);
<input ref={inputRef} />
useEffect(() => inputRef.current.focus(), []);But the more interesting use is as a mutable container that survives re-renders without triggering them. This is fundamentally different from useState:
setState(x)- schedules a re-renderref.current = x- updates silently, no re-render
Practical applications:
// 1. Store a timer ID
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setTimeout(doSomething, 1000);
return () => clearTimeout(timerRef.current);
}, []);
// 2. Track whether it's the first render
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return; // skip on mount
}
doSomethingOnUpdate();
});
// 3. Always-current callback (avoid stale closures in event listeners)
const callbackRef = useRef(callback);
useEffect(() => { callbackRef.current = callback; }, [callback]);
useEffect(() => {
window.addEventListener('keydown', e => callbackRef.current(e));
}, []); // listener never recreates, always calls fresh callbackThe rule: if the value doesn't need to appear in the UI, don't put it in state. Use a ref.
usePrevious - the classic ref pattern
function usePrevious(value) {
const ref = useRef(undefined);
useEffect(() => {
ref.current = value; // runs AFTER render
}, [value]);
return ref.current; // returns value from PREVIOUS render
}
const prevCount = usePrevious(count);
// During render: ref.current still holds the last render's value
// After render: effect updates it for next timeuseMemo - Remembering Results
useMemo caches the return value of a function and only recomputes when dependencies change.
const sorted = useMemo(
() => [...items].sort(complexCompareFn),
[items] // recalculate only when items changes
);When it actually matters
1. Genuinely expensive computation
// Without memo: recalculates on every render
const stats = calculateStatsAcross10kItems(data);
// With memo: recalculates only when data changes
const stats = useMemo(() => calculateStatsAcross10kItems(data), [data]);2. Stable reference for a memo component below
const Chart = React.memo(ChartImpl);
// Without useMemo: config is a new object every render → memo is useless
return <Chart config={{ size: 10, mode: 'auto' }} />;
// With useMemo: same reference → memo works as intended
const config = useMemo(() => ({ size: 10, mode: 'auto' }), []);
return <Chart config={config} />;3. Stable reference as a dependency of another hook
const options = useMemo(() => ({ retry: 3, timeout: 5000 }), []);
useEffect(() => {
fetchWith(options);
}, [options]); // won't fire on every render because options reference is stableWhen it's pointless (and just adds noise)
// Too cheap to bother - the memo overhead is higher than the savings
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// Just write: const fullName = `${first} ${last}`;
// Not passed to memo components or hooks
const doubled = useMemo(() => count * 2, [count]);
return <div>{doubled}</div>; // div always re-renders with parent anywayuseCallback - Remembering Functions
useCallback(fn, deps) is literally useMemo(() => fn, deps). It returns the same function reference between renders as long as deps don't change.
// Without useCallback: new function reference on every render
const handleClick = () => doStuff(id);
// With useCallback: same reference if id hasn't changed
const handleClick = useCallback(() => doStuff(id), [id]);The only situations where it matters
1. The function is a prop to a React.memo component
const Row = React.memo(RowImpl);
// Without useCallback: new function every render → memo never skips
return <Row onClick={() => select(item.id)} item={item} />;
// With useCallback: stable reference → memo skips when item hasn't changed
const handleClick = useCallback(() => select(item.id), [item.id]);
return <Row onClick={handleClick} item={item} />;2. The function is a dependency of another hook
const fetchUser = useCallback(() => api.getUser(userId), [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // won't loop because fetchUser is stable when userId is stableThe mistake everyone makes
// Wrapping onClick on a plain div with useCallback
const handleClick = useCallback(() => setX(1), []);
return <div onClick={handleClick} />;
// div ALWAYS re-renders when parent does
// useCallback here accomplishes nothinguseCallback only helps when it prevents a downstream component from re-rendering. No memo component below = no benefit.
React.memo - Putting It Together
React.memo wraps a component and skips re-rendering if all props are shallowly equal to previous render.
const ExpensiveList = React.memo(function ExpensiveListImpl({ items, onSelect }) {
return (
<ul>
{items.map(item => <li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>)}
</ul>
);
});It only works if the props are actually stable. This is why useMemo and useCallback exist - to make props stable so memo can do its job.
function Parent() {
const [count, setCount] = useState(0);
const items = useMemo(() => computeItems(), []); // stable reference
const onSelect = useCallback((id) => select(id), []); // stable reference
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveList items={items} onSelect={onSelect} /> {/* never re-renders */}
</>
);
}Without useMemo and useCallback, clicking the counter would re-render ExpensiveList on every click even though nothing about the list changed.
The Interview Test
Interviewers use this pattern to distinguish candidates:
const Child = React.memo(({ onClick, label }) => (
<button onClick={onClick}>{label}</button>
));
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child label="Click me" onClick={() => console.log('clicked')} />
</>
);
}Question: Does memo on Child actually prevent re-renders?
Answer: No. () => console.log('clicked') creates a new function reference on every render. memo does a shallow comparison and sees a new onClick prop every time. Child re-renders on every counter increment.
Fix: useCallback(() => console.log('clicked'), []) on the onClick handler.