Back to writing
ReactJan 2026·7 min read

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 them
  • useMemo - remembers the result of a calculation
  • useCallback - 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-render
  • ref.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 callback

The 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 time

useMemo - 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 stable

When 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 anyway

useCallback - 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 stable

The 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 nothing

useCallback 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.