React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

memoizationperformancecallbackshooks

useCallback Hook

Cache a function definition between renders to maintain a stable reference.

Recipe

Quick-reference recipe card — copy-paste ready.

const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
 
// Common pattern: stabilize a callback passed to a memoized child
const handleDelete = useCallback((id: string) => {
  setItems((prev) => prev.filter((item) => item.id !== id));
}, []);

When to reach for this: You pass a function as a prop to a React.memo child, or as a dependency in a useEffect or useMemo, and need it to not change on every render.

Working Example

"use client";
 
import { memo, useCallback, useState } from "react";
 
interface TodoItemProps {
  id: number;
  text: string;
  onDelete: (id: number) => void;
}
 
const TodoItem = memo(function TodoItem({ id, text, onDelete }: TodoItemProps) {
  console.log(`Rendering: ${text}`);
  return (
    <li className="flex items-center justify-between py-1">
      <span>{text}</span>
      <button onClick={() => onDelete(id)} className="text-red-500 text-sm">
        Delete
      </button>
    </li>
  );
});
 
export function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React" },
    { id: 2, text: "Build an app" },
    { id: 3, text: "Ship it" },
  ]);
  const [input, setInput] = useState("");
 
  const handleDelete = useCallback((id: number) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);
 
  const handleAdd = useCallback(() => {
    if (!input.trim()) return;
    setTodos((prev) => [...prev, { id: Date.now(), text: input }]);
    setInput("");
  }, [input]);
 
  return (
    <div className="space-y-2">
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="border rounded px-2 py-1 flex-1"
          placeholder="New todo"
        />
        <button onClick={handleAdd} className="px-3 py-1 border rounded">
          Add
        </button>
      </div>
      <ul>
        {todos.map((todo) => (
          <TodoItem key={todo.id} id={todo.id} text={todo.text} onDelete={handleDelete} />
        ))}
      </ul>
    </div>
  );
}

What this demonstrates:

  • handleDelete is wrapped in useCallback with [] dependencies, so its reference never changes
  • TodoItem is wrapped in React.memo, so it only re-renders when its props change
  • Without useCallback, typing in the input would re-render every TodoItem because handleDelete would be a new function each time
  • handleAdd depends on input, so it updates when the input changes — that's correct

Deep Dive

How It Works

  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
  • On the first render, React stores the function and its dependencies
  • On subsequent renders, React compares each dependency using Object.is
  • If all dependencies are unchanged, React returns the previously stored function (same reference)
  • If any dependency changed, React stores and returns the new function

Parameters & Return Values

ParameterTypeDescription
fn(...args: A) => RThe function to memoize
dependenciesunknown[]Array of reactive values used inside the function
ReturnTypeDescription
memoizedFn(...args: A) => RCached function with a stable reference

Variations

Stabilize an event handler for useEffect:

const fetchData = useCallback(async () => {
  const res = await fetch(`/api/items?page=${page}`);
  setData(await res.json());
}, [page]);
 
useEffect(() => {
  fetchData();
}, [fetchData]);

With generics:

const handleSelect = useCallback(<T extends { id: string }>(item: T) => {
  setSelectedId(item.id);
}, []);

Stable callback with no dependencies (common for updater patterns):

const toggle = useCallback(() => {
  setOpen((prev) => !prev);
}, []);

TypeScript Notes

// TypeScript infers the callback type from usage
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  setQuery(e.target.value);
}, []);
// handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
 
// When passing to a child, the child's prop type constrains the callback type
interface Props {
  onSelect: (id: string) => void;
}

Gotchas

  • useCallback without React.memo — Wrapping a callback in useCallback does nothing if the child is not memoized with React.memo. Fix: Only use useCallback when the consumer actually benefits from a stable reference.

  • Stale closure — If you omit a dependency, the callback closes over an old value. Fix: Include all reactive values in the dependency array, or use updater functions (setState(prev => ...)) to avoid the dependency.

  • Over-memoizing — Wrapping every function in useCallback adds cognitive overhead and memory usage. Fix: Only memoize when passing to React.memo children, or when used as a useEffect / useMemo dependency.

  • Dependencies that change every render — If a dependency is an unstable object or array, the callback reference changes every render anyway. Fix: Memoize the dependency with useMemo or restructure to use primitives.

Alternatives

AlternativeUse WhenDon't Use When
Inline functionChild is not memoized, or the function is only used in the same componentFunction is passed to a React.memo child
useMemoYou need to memoize a non-function valueYou need to memoize a function
useReducer dispatchMultiple children need to trigger state changes — dispatch is always stableSimple single-value updates
Ref callbackYou need a stable function that always reads the latest valuesYou want the function identity to change when deps change

Rule of thumb: Start without useCallback. Add it when you profile and find unnecessary re-renders in memoized children, or when a function used as an effect dependency keeps re-triggering the effect.

FAQs

Does useCallback do anything if the child component is not wrapped in React.memo?
  • No. Without React.memo, the child re-renders on every parent render regardless of prop stability.
  • useCallback only prevents re-renders when the consumer checks prop equality (via React.memo or dependency arrays).
  • Start without useCallback and add it when profiling shows unnecessary re-renders.
How is useCallback related to useMemo?
  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
  • useCallback caches the function itself; useMemo caches the return value of a function.
  • Use useCallback for functions and useMemo for non-function values.
Why does useCallback with an empty dependency array [] create a stable reference?
  • With [], React never detects a dependency change, so it returns the same function reference forever.
  • This is safe when the callback uses only updater functions (setState(prev => ...)) that don't depend on external values.
  • If the callback reads state or props directly, those values become stale.
Gotcha: What happens if I omit a dependency from useCallback?
  • The callback closes over the stale value from the render when it was created.
  • This causes bugs where the callback reads outdated state or props.
  • Include all reactive values in the dependency array, or use updater functions to avoid the dependency.
When should I use useCallback to stabilize a function for useEffect?
const fetchData = useCallback(async () => {
  const res = await fetch(`/api/items?page=${page}`);
  setData(await res.json());
}, [page]);
 
useEffect(() => {
  fetchData();
}, [fetchData]); // Only re-runs when page changes
Why would useReducer's dispatch be a better alternative than useCallback in some cases?
  • dispatch from useReducer is always stable -- it never changes between renders.
  • You can pass dispatch to multiple children without useCallback or React.memo concerns.
  • It's ideal when multiple children need to trigger different state changes.
How does TypeScript infer the type of a useCallback function?
const handleChange = useCallback(
  (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  },
  []
);
// Type: (e: React.ChangeEvent<HTMLInputElement>) => void
  • TypeScript infers the callback type from the function signature you provide.
Gotcha: Why does my useCallback reference change every render even with dependencies?
  • One of the dependencies is an unstable reference (a new object or array created each render).
  • Even though the values inside the object haven't changed, Object.is sees a new reference.
  • Memoize the dependency with useMemo or restructure to use primitive values.
Should I wrap every event handler in useCallback?
  • No. Wrapping every function adds cognitive overhead and memory usage.
  • Only memoize when passing to React.memo children or using as a useEffect/useMemo dependency.
  • For inline handlers on native elements, the overhead of useCallback is not worthwhile.
How do I use useCallback with a generic type parameter in TypeScript?
const handleSelect = useCallback(
  <T extends { id: string }>(item: T) => {
    setSelectedId(item.id);
  },
  []
);
  • The generic is placed on the inner function, not on useCallback itself.
  • useMemo — memoize computed values rather than functions
  • useEffect — stable callbacks prevent unnecessary effect re-runs
  • useRef — alternative for always-current values without re-render triggers
  • Custom Hooks — custom hooks often return memoized callbacks