useCallback Hook

Difficulty: Intermediate

useCallback is a React hook that memoizes a function definition. It returns the same function reference between re-renders as long as its dependencies haven't changed. This is essentially useMemo for functions - in fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). The primary purpose is to maintain referential stability for functions passed as props or used as dependencies.

The main use case for useCallback is preventing unnecessary re-renders of child components that are wrapped in React.memo. When a parent component re-renders, it creates new function instances for all inline functions. If an onClick handler or callback is passed to a React.memo child, the new function reference causes the memo comparison to fail, triggering a re-render. useCallback solves this by returning the same function reference when dependencies haven't changed.

useCallback is also important when functions are used as dependencies in other hooks. If you pass a function to useEffect's dependency array, the effect re-runs whenever the function reference changes. Without useCallback, the function is recreated every render, causing the effect to run every render - defeating the purpose of the dependency array. useCallback stabilizes the reference so the effect only runs when the function's dependencies actually change.

Like useMemo, useCallback should be used judiciously. Every useCallback adds memory overhead (storing the function and dependencies) and cognitive complexity. If a component doesn't pass callbacks to memoized children and doesn't use functions as effect dependencies, useCallback provides no benefit. Profile and measure before adding useCallback throughout your codebase.

A common pattern is pairing useCallback with React.memo on child components. The parent uses useCallback for handlers, and the child is wrapped in React.memo. This combination ensures the child only re-renders when its props genuinely change. Without both pieces, the optimization doesn't work - useCallback alone doesn't prevent re-renders, and React.memo alone doesn't help if functions are recreated each render.

Code examples

useCallback prevents reference changes

function Parent() {
  const [count, setCount] = React.useState(0);

  // Without useCallback: new function every render
  const unstableHandler = () => {
    console.log('clicked');
  };

  // With useCallback: same function reference
  const stableHandler = React.useCallback(() => {
    console.log('clicked');
  }, []);

  console.log('Unstable creates new ref each render: true');
  console.log('Stable keeps same ref between renders: true');
  console.log('useCallback returns a function:', typeof stableHandler === 'function');

  return null;
}

Parent();

useCallback returns the same function reference between renders when dependencies haven't changed. The unstable version creates a new function on every render.

useCallback with React.memo child

// Simulating React.memo behavior
function MemoizedChild(props) {
  console.log('Child rendered with handler type:', typeof props.onClick);
  return null;
}

function Parent() {
  const [count, setCount] = React.useState(0);

  const handleClick = React.useCallback(() => {
    console.log('Button clicked, count:', count);
  }, [count]);

  console.log('Parent rendered, count:', count);
  MemoizedChild({ onClick: handleClick });
  return null;
}

Parent();

The handleClick function is memoized with [count] as dependency. It only gets a new reference when count changes. A React.memo-wrapped child receiving this callback would skip re-renders when only other parent state changes.

useCallback as effect dependency

function SearchComponent({ query }) {
  const fetchResults = React.useCallback(() => {
    console.log('Fetching results for:', query);
    return ['result1', 'result2'];
  }, [query]);

  React.useEffect(() => {
    const results = fetchResults();
    console.log('Got results:', results.length);
  }, [fetchResults]); // Stable reference prevents infinite loop

  console.log('SearchComponent rendered');
  return null;
}

SearchComponent({ query: 'hooks' });

Without useCallback, fetchResults would be a new function every render, causing the useEffect to run every render. useCallback ensures the effect only re-runs when query changes.

Key points

Concepts covered

useCallback, function memoization, preventing re-renders, React.memo, referential equality