useCallback Hook 🔁

1. Introduction 👋

useCallback is a performance-focused Hook that lets you memoize a function definition across renders. This tutorial explores how it works, its relationship to useMemo, React.memo, and useEffect, and — just as importantly — when it's better left out.

Information

This tutorial assumes familiarity with useState, useMemo, and Components, covered in earlier tutorials.

2. What is useCallback? 🤔

useCallback returns a memoized version of a function that only changes if one of its dependencies has changed, preventing a new function reference from being created on every render.

Code Snippet

import { useCallback } from 'react';

function TodoList({ onAddTodo }) {
  const handleAdd = useCallback((text) => {
    onAddTodo({ id: crypto.randomUUID(), text });
  }, [onAddTodo]);

  return <TodoForm onSubmit={handleAdd} />;
}
>>"useCallback is a React Hook that lets you cache a function definition between re-renders." — React Documentation

3. Why Use useCallback? 💡

  • Stable References: Prevents unnecessary re-renders in memo-wrapped child components.
  • Effect Stability: Avoids re-running effects that depend on a function passed in as a dependency.
  • Custom Hook Consistency: Ensures functions returned from custom Hooks remain stable for consumers.

Caution

Like useMemo, useCallback is a performance optimization — it should never be relied upon for program correctness.

4. Function Memoization 🧠

In JavaScript, functions are reference types — every time a component renders, any function defined inside it is a brand-new object, even if its logic is identical to the previous render's version.

Code Snippet

function Component() {
  // A NEW function is created on every single render
  const handleClick = () => console.log("Clicked");
}

Note

This matters primarily when that function reference is compared elsewhere — like in a memo component's props check or a useEffect dependency array.

5. How useCallback Works âš™ī¸

On first render, React stores the function along with its dependencies. On later renders, if the dependencies are unchanged, React returns the original function reference instead of the newly created one.

Component Re-renders
React Checks Dependencies
Unchanged → Return Cached Function Reference
Changed → Cache and Return New Function

6. Syntax 📐

Code Snippet

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Tip

useCallback(fn, deps) is functionally equivalent to useMemo(() => fn, deps) — it's essentially a convenience wrapper for the common case of memoizing a function.

7. Dependency Array 📋

Code Snippet

const handleSearch = useCallback((query) => {
  fetchResults(query, category); // reads 'category' from outer scope
}, [category]); // must include 'category'

Important

Any variable from the component's scope that's referenced inside the callback must be included in the dependency array, or the function will capture a stale value.

8. Memoizing Event Handlers đŸ–ąī¸

Code Snippet

function ProductCard({ product, onAddToCart }) {
  const handleClick = useCallback(() => {
    onAddToCart(product.id);
  }, [product.id, onAddToCart]);

  return <button onClick={handleClick}>Add to Cart</button>;
}

Tip

Memoizing event handlers is most valuable when they're passed down to memoized child components — for a plain <button> with no memoized children, it usually provides no benefit.

9. Preventing Unnecessary Re-renders đŸšĢ

Problem: New Function Reference Breaks memo()

const ExpensiveList = memo(function ExpensiveList({ onSelect }) {
  console.log("Rendering list...");
  return <div>{/* expensive rendering */}</div>;
});

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

  // ❌ New function every render — defeats memo() on ExpensiveList
  const handleSelect = (id) => console.log(id);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveList onSelect={handleSelect} />
    </>
  );
}

Solution: useCallback Stabilizes the Reference

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

  // ✅ Stable reference across re-renders
  const handleSelect = useCallback((id) => console.log(id), []);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveList onSelect={handleSelect} />
    </>
  );
}

10. Passing Stable Callback References 🔗

Stable references matter most when a callback flows into multiple layers of memoized components, or into other Hooks that compare dependencies by reference.

Code Snippet

function SearchPage() {
  const [results, setResults] = useState([]);

  const handleSearch = useCallback((query) => {
    fetchResults(query).then(setResults);
  }, []); // setResults from useState is always stable

  return <SearchBar onSearch={handleSearch} />;
}

11. useCallback with React.memo 🧩

React.memo skips re-rendering a component when its props are shallowly equal to the previous render's props. Without useCallback, function props defeat this check every time.

Code Snippet

const Button = memo(function Button({ onClick, label }) {
  console.log("Button rendered:", label);
  return <button onClick={onClick}>{label}</button>;
});

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

  const handleSave = useCallback(() => console.log("Saving..."), []);

  return (
    <>
      <Button onClick={handleSave} label="Save" />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  );
}
// "Save" button never re-renders when 'count' changes

12. useCallback with useEffect 🔄

When a function is used inside a useEffect and also listed as a dependency, memoizing it with useCallback prevents the effect from re-running on every render.

Code Snippet

function ChatRoom({ roomId }) {
  const connectToRoom = useCallback(() => {
    return createConnection(roomId);
  }, [roomId]);

  useEffect(() => {
    const connection = connectToRoom();
    return () => connection.disconnect();
  }, [connectToRoom]); // stable unless 'roomId' changes
}

Tip

This pattern is especially useful when a function is defined in a custom Hook and consumed by an effect elsewhere.

13. useCallback with Custom Hooks đŸ› ī¸

Code Snippet

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

  const increment = useCallback(() => setCount((c) => c + 1), []);
  const decrement = useCallback(() => setCount((c) => c - 1), []);
  const reset = useCallback(() => setCount(initial), [initial]);

  return { count, increment, decrement, reset };
}

Best Practice

Memoizing functions returned from a custom Hook ensures consumers can safely use them in their own useEffect or memo dependency checks without unexpected re-runs.

14. useCallback vs useMemo âš”ī¸

AspectuseCallbackuseMemo
CachesA function referenceA computed value
Equivalent TouseMemo(() => fn, deps)N/A
Typical UseStable callbacks for props or effectsExpensive calculations or derived data

15. useCallback vs useRef âš”ī¸

AspectuseCallbackuseRef
PurposeMemoize a function based on dependenciesStore any mutable value with no automatic updates
Update TriggerAutomatic, based on dependency arrayManual — you assign .current yourself

Note

A ref can store the latest version of a callback for advanced patterns (like avoiding a dependency entirely), but this trades away the automatic freshness that useCallback provides.

16. Performance Optimization ⚡

  • useCallback itself has a small cost — the dependency comparison on every render.
  • Its benefit only materializes when the stable reference actually prevents something downstream (a re-render, an effect re-run).
  • Overusing it on functions with no memoized consumers adds overhead without any payoff.

Warning

Wrapping a handler in useCallback that's only ever passed to a plain, non-memoized <button> provides no measurable benefit.

17. Dependency Management 📐

Code Snippet

function SearchBar({ onSearch, category }) {
  const handleChange = useCallback((query) => {
    onSearch(query, category); // both must be listed
  }, [onSearch, category]);
}

Tip

Enable eslint-plugin-react-hooks's exhaustive-deps rule — it automatically flags missing dependencies in useCallback just as it does for useEffect and useMemo.

18. Stale Closures đŸ•°ī¸

❌ Stale Closure — Missing Dependency

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

  const logCount = useCallback(() => {
    console.log(count); // always logs the INITIAL count
  }, []); // ❌ missing 'count' dependency
}

✅ Fixed with the Correct Dependency

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

  const logCount = useCallback(() => {
    console.log(count); // always logs the CURRENT count
  }, [count]); // ✅ correctly listed
}

Danger

A stale closure in a memoized callback is a particularly sneaky bug — the function looks correct and works initially, but silently uses outdated values after state changes.

19. Common Use Cases 🎨

  • Passing callbacks to memoized child components to prevent unnecessary re-renders.
  • Stabilizing a function used as a dependency in useEffect.
  • Returning stable functions from custom Hooks for predictable consumer behavior.
  • Optimizing components in large, frequently re-rendering lists.

20. When Not to Use useCallback đŸšĢ

  • When the function is only used inside JSX event handlers on native DOM elements (like a plain button).
  • When the function isn't passed to a memo-wrapped component or used as a dependency elsewhere.
  • As a blanket default applied to every function "just in case."

Best Practice

Write the component without useCallback first. Add it only after profiling reveals an actual re-render problem tied to an unstable function reference.

21. TypeScript with useCallback 🔷

Code Snippet

interface Item {
  id: string;
  name: string;
}

function ItemList({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
  const handleSelect = useCallback(
    (id: string) => {
      onSelect(id);
    },
    [onSelect]
  );

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

Tip

TS infers the function's parameter and return types automatically from the callback definition — explicit generics are rarely required for useCallback.

22. Best Practices 🌟

  1. Use useCallback only when a stable reference genuinely prevents unnecessary work downstream.
  2. Pair it with React.memo on the receiving component for the optimization to actually take effect.
  3. Always include every value referenced inside the function in the dependency array.
  4. Return memoized functions from custom Hooks when consumers may use them in effects or memoized components.
  5. Profile before optimizing — confirm the re-render issue is real before adding useCallback.

23. Common Mistakes đŸšĢ

  • Wrapping every function in useCallback regardless of whether it provides any benefit.
  • Omitting dependencies, creating stale closures that silently use outdated values.
  • Using useCallback without pairing it with React.memo on the consuming component, gaining no actual benefit.
  • Assuming useCallback improves performance automatically, without measuring first.

Danger

Memoizing a callback with useCallback but passing it to a non-memoized child component provides zero benefit — the child re-renders regardless, since it doesn't check prop equality at all.

24. Frequently Asked Questions ❓

Question

Does useCallback make the function run faster?

Answer

No. It doesn't change how the function executes — it only preserves the function's reference across renders, which matters for reference-equality checks elsewhere.

Question

What's the difference between useCallback(fn, deps) and useMemo(() => fn, deps)?

Answer

They are functionally equivalent — useCallback is simply a more convenient, readable syntax specifically for the common case of memoizing a function.

Question

Should I wrap every event handler in useCallback?

Answer

No. Only functions passed to memoized components or used as dependencies in other Hooks benefit meaningfully — wrapping everything adds unnecessary overhead.

25. Summary 📝

useCallback preserves a stable function reference across renders, most valuable when paired with React.memo or used as a dependency in useEffect. Like useMemo, it's a targeted performance optimization — applied deliberately after profiling, not as a universal default.

Summary

With useCallback, useMemo, and useRef all covered, a strong next step is exploring React.memo in depth, along with broader Performance Optimization strategies for large-scale React applications.