Skip to main content
useCallback returns a memoized version of the callback that only changes if one of the dependencies has changed. This is useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Signature

function useCallback<T extends Function>(
  callback: T,
  inputs: ReadonlyArray<unknown>
): T

Parameters

callback
T extends Function
required
The callback function to memoize. This function will be returned on subsequent renders if the dependencies haven’t changed.
inputs
ReadonlyArray<unknown>
required
An array of dependencies. The callback will only be recreated if one of these values changes (compared using ===).

Returns

Returns the memoized callback function of type T. On the initial render, it returns the passed callback. On subsequent renders, it returns the same callback reference if dependencies haven’t changed, or a new callback if they have.

Basic Usage

import { useCallback, useState } from 'preact/hooks';
import { memo } from 'preact/compat';

const Button = memo(({ onClick, children }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>{children}</button>;
});

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

  // Memoize callback to prevent Button re-renders
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []); // No dependencies, never recreated

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={increment}>Increment</Button>
      <button onClick={() => setOtherState(s => s + 1)}>
        Other State: {otherState}
      </button>
    </div>
  );
}

With Dependencies

function SearchResults({ query, category }) {
  const [results, setResults] = useState([]);

  // Recreate callback when query or category changes
  const handleSearch = useCallback(async () => {
    const data = await fetch(`/api/search?q=${query}&cat=${category}`);
    const json = await data.json();
    setResults(json);
  }, [query, category]);

  return (
    <div>
      <button onClick={handleSearch}>Search</button>
      <ResultsList results={results} />
    </div>
  );
}

Passing to Child Components

const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  console.log('TodoItem rendered:', todo.id);
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

Event Handlers

function Form({ userId, onSubmit }) {
  const [formData, setFormData] = useState({ name: '', email: '' });

  const handleChange = useCallback((field) => {
    return (e) => {
      setFormData(prev => ({
        ...prev,
        [field]: e.target.value
      }));
    };
  }, []);

  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    onSubmit({ ...formData, userId });
  }, [formData, userId, onSubmit]);

  return (
    <form onSubmit={handleSubmit}>
      <input value={formData.name} onChange={handleChange('name')} />
      <input value={formData.email} onChange={handleChange('email')} />
      <button type="submit">Submit</button>
    </form>
  );
}

With useEffect

function DataFetcher({ url, params }) {
  const [data, setData] = useState(null);

  const fetchData = useCallback(async () => {
    const query = new URLSearchParams(params).toString();
    const response = await fetch(`${url}?${query}`);
    const json = await response.json();
    setData(json);
  }, [url, params]);

  useEffect(() => {
    fetchData();
  }, [fetchData]); // Safe to use as dependency

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Debounced Callbacks

function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');
  const timeoutRef = useRef(null);

  const debouncedSearch = useCallback((value) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      onSearch(value);
    }, 500);
  }, [onSearch]);

  const handleChange = useCallback((e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  }, [debouncedSearch]);

  return <input value={query} onChange={handleChange} />;
}

Callback Factories

function ItemList({ items, onItemClick }) {
  // Create memoized callback factory
  const createClickHandler = useCallback((item) => {
    return () => onItemClick(item);
  }, [onItemClick]);

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <button onClick={createClickHandler(item)}>
            {item.name}
          </button>
        </li>
      ))}
    </ul>
  );
}

With Context

const ActionsContext = createContext();

function ActionsProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  // Memoize all action creators
  const actions = useMemo(() => ({
    addItem: (item) => dispatch({ type: 'ADD_ITEM', payload: item }),
    removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', payload: id }),
    updateItem: (item) => dispatch({ type: 'UPDATE_ITEM', payload: item })
  }), []);

  return (
    <ActionsContext.Provider value={actions}>
      {children}
    </ActionsContext.Provider>
  );
}

function Component() {
  const { addItem } = useContext(ActionsContext);

  // Safe to use in effects/callbacks
  const handleAdd = useCallback(() => {
    addItem({ id: Date.now(), text: 'New item' });
  }, [addItem]);

  return <button onClick={handleAdd}>Add Item</button>;
}

Relationship with useMemo

// These are equivalent:
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

const memoizedCallback = useMemo(() => {
  return () => {
    doSomething(a, b);
  };
}, [a, b]);
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). It’s a convenience hook specifically for memoizing functions.
Use useCallback when passing callbacks to optimized child components (wrapped in memo) that rely on reference equality to prevent unnecessary renders.
Don’t use useCallback everywhere. Only use it when:
  • Passing callbacks to memoized child components
  • Using the callback as a dependency in other hooks like useEffect
  • The callback is expensive to create

Build docs developers (and LLMs) love