React useCallback

useCallback returns a memoized version of a function so it keeps the same reference across re-renders as long as its dependencies haven't changed.

Why Function Identity Matters

Every time a component re-renders, any function you define inside it — including inline handlers like () => doThing() — is a brand-new function object, even if its logic is identical to the previous render's. That's irrelevant for a plain <button onClick={...}>, but it matters the moment that function is a dependency of useEffect, or a prop passed to a component wrapped in React.memo, because a 'new' reference looks like a real change.

What useCallback Does

useCallback(fn, deps) returns the same function reference across renders as long as none of the values in deps have changed since the last render. Internally it's closely related to useMemo — useCallback(fn, deps) behaves like useMemo(() => fn, deps) — it just saves you writing the wrapping arrow function yourself.

  • Passing a callback down to a child wrapped in React.memo, so the child doesn't re-render just because the parent re-rendered
  • Using a function as a dependency of useEffect, useMemo, or another useCallback, so the effect doesn't re-run every render
  • Passing a stable handler to a custom hook that relies on referential equality internally
  • Debouncing or throttling a function where the wrapper must stay the same instance across renders

Without useCallback: A Memoized Child Re-renders Anyway

import React, { useState } from 'react';

const ExpensiveList = React.memo(function ExpensiveList({ onItemClick }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      <li onClick={() => onItemClick(1)}>Item 1</li>
      <li onClick={() => onItemClick(2)}>Item 2</li>
    </ul>
  );
});

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

  // A new function is created on every render...
  function handleItemClick(id) {
    console.log('Clicked item', id);
  }

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* ...so ExpensiveList re-renders every time, even though
          React.memo is supposed to skip unchanged props. */}
      <ExpensiveList onItemClick={handleItemClick} />
    </div>
  );
}

export default Parent;

With useCallback: The Child Skips Re-rendering

import React, { useState, useCallback } from 'react';

const ExpensiveList = React.memo(function ExpensiveList({ onItemClick }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      <li onClick={() => onItemClick(1)}>Item 1</li>
      <li onClick={() => onItemClick(2)}>Item 2</li>
    </ul>
  );
});

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

  const handleItemClick = useCallback((id) => {
    console.log('Clicked item', id);
  }, []); // no dependencies: the function never needs to change

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* handleItemClick keeps the same reference across renders,
          so React.memo correctly skips re-rendering ExpensiveList. */}
      <ExpensiveList onItemClick={handleItemClick} />
    </div>
  );
}

export default Parent;
Note: useCallback only prevents the function object from changing — it doesn't stop the component that defines it from re-rendering. The gain shows up downstream, in children or effects that specifically check the function's identity.

Stabilizing a Debounced Search Handler

import { useCallback, useRef, useState } from 'react';

function useDebouncedCallback(callback, delay) {
  const timeoutRef = useRef(null);

  return useCallback(
    (...args) => {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = setTimeout(() => callback(...args), delay);
    },
    [callback, delay]
  );
}

function SearchBox({ onSearch }) {
  const debouncedSearch = useDebouncedCallback(onSearch, 300);

  return (
    <input
      type="text"
      placeholder="Search..."
      onChange={(e) => debouncedSearch(e.target.value)}
    />
  );
}

export default function App() {
  const [lastSearch, setLastSearch] = useState('');

  return (
    <div>
      <SearchBox
        onSearch={(value) => {
          console.log('Searching for', value);
          setLastSearch(value);
        }}
      />
      <p>Last search: {lastSearch}</p>
    </div>
  );
}
ScenarioDoes useCallback help?
Inline onClick on a plain <button>No — nothing checks its identity
Callback prop passed to a React.memo childYes — prevents an unnecessary child re-render
Function used inside a useEffect dependency arrayYes — prevents the effect from re-running every render
Function only ever called directly in the same component's JSXNo — memoizing adds overhead with no downstream benefit
Function returned from a custom hook that other memoized hooks depend onYes — keeps consumers' dependency arrays stable
Note: Wrapping every function in useCallback 'just in case' adds a dependency array to maintain and a small memoization overhead for no benefit, since most child components aren't memoized and most functions aren't effect dependencies. Reach for it when you can point to the specific re-render or re-run it prevents.

Exercise: React useCallback

What does useCallback actually memoize?