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.