React Tutorial 0/120 lessons ~6 min read Lesson 36

    useCallback

    useCallback in React covers caching function references for memoized children, Hook dependencies, custom Hook APIs, and intentional performance optimization.

    Course progress0%
    Focus
    48 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    useCallback is a React Hook that lets you cache a function definition between renders.

    It is mainly useful when a function's identity matters—for example, when passing a callback to a memoized child component or when a function is used as a dependency of another Hook.

    The basic pattern is:

    javascript
    const cachedFunction = useCallback(
    () => {
    // function logic
    },
    [dependencies]
    );

    A useful mental model is:

    text
    Component renders
    ↓
    Does the dependency list change?
    │
    ┌───┴───┐
    │ │
    No Yes
    │ │
    Reuse Create
    same new
    function function

    The most important point is:

    useCallback memoizes a function reference, not the result of calling that function.

    Why Does Function Identity Matter?

    In JavaScript, functions are objects and therefore have identity.

    Consider:

    javascript
    function App() {
    const handleClick = () => {
    console.log("Clicked");
    };
    return <button onClick={handleClick}>Click</button>;
    }

    Whenever App renders, the function expression can create a new function:

    text
    Render 1 → handleClick → Function A
    Render 2 → handleClick → Function B
    Render 3 → handleClick → Function C

    Even though the function does the same thing, these are different references.

    javascript
    Function A !== Function B

    This matters when another part of the application cares about reference equality.

    Importing useCallback

    Import it from React:

    javascript
    import { useCallback } from "react";

    It can be used with other Hooks:

    javascript
    import {
    memo,
    useCallback,
    useState
    } from "react";

    Basic Syntax

    The basic syntax is:

    javascript
    const handleClick = useCallback(
    () => {
    // logic
    },
    []
    );

    With dependencies:

    javascript
    const handleClick = useCallback(
    () => {
    doSomething(userId);
    },
    [userId]
    );

    The function is recreated when its dependencies change.

    useCallback Returns a Function

    This is important.

    javascript
    const handleClick = useCallback(
    () => {
    console.log("Hello");
    },
    []
    );

    The result of useCallback is the function itself.

    You can pass it:

    javascript
    <button onClick={handleClick}>
    Click
    </button>

    You can also call it manually:

    javascript
    handleClick();

    The Hook does not execute the function for you.

    useCallback vs useMemo

    These Hooks are closely related.

    useMemo

    useMemo caches the result of a calculation:

    javascript
    const total = useMemo(
    () => calculateTotal(items),
    [items]
    );

    useCallback

    useCallback caches the function itself:

    javascript
    const handleDelete = useCallback(
    (id) => {
    deleteItem(id);
    },
    []
    );

    Mental model:

    text
    useMemo
    ↓
    "Remember this value."
    useCallback
    ↓
    "Remember this function."

    Conceptually, these are related:

    javascript
    useCallback(fn, dependencies)

    can be thought of as roughly equivalent to:

    javascript
    useMemo(() => fn, dependencies)

    The dedicated useCallback API makes the intent clearer when what you want to preserve is a function reference.

    useCallback Does Not Cache the Function's Result

    Consider:

    javascript
    const calculateTotal = useCallback(() => {
    return calculateExpensiveTotal(items);
    }, [items]);

    This does not cache the total.

    It caches the function:

    text
    calculateTotal function

    When you call:

    javascript
    calculateTotal();

    the calculation still executes.

    If you want to cache the result:

    javascript
    const total = useMemo(
    () => calculateExpensiveTotal(items),
    [items]
    );

    Use the correct Hook for the problem.

    Why Functions Change Between Renders

    Consider:

    javascript
    function Parent() {
    const [count, setCount] = useState(0);
    const handleClick = () => {
    console.log("Clicked");
    };
    return (
    <div>
    <p>{count}</p>
    <button onClick={() => setCount(count + 1)}>
    Increment
    </button>
    <Child onClick={handleClick} />
    </div>
    );
    }

    When count changes:

    text
    Parent renders
    ↓
    New handleClick function
    ↓
    Child receives new function reference

    Even though:

    javascript
    handleClick

    does exactly the same thing, its identity changed.

    This becomes important when Child is optimized with memo.

    React.memo and useCallback

    Consider a memoized child:

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

    Now:

    javascript
    function Parent() {
    const [count, setCount] = useState(0);
    const handleClick = () => {
    console.log("Clicked");
    };
    return (
    <div>
    <button onClick={() => setCount(count + 1)}>
    Count: {count}
    </button>
    <Child onClick={handleClick} />
    </div>
    );
    }

    Every parent render creates a new handleClick.

    Therefore:

    text
    Parent renders
    ↓
    New handleClick reference
    ↓
    Child receives changed prop
    ↓
    memo cannot skip Child

    Using useCallback:

    javascript
    const handleClick = useCallback(() => {
    console.log("Clicked");
    }, []);

    Now the function reference can remain stable:

    text
    Parent renders
    ↓
    handleClick reference unchanged
    ↓
    Child receives same function prop
    ↓
    memo can skip unnecessary render

    This is one of the most important real-world uses of useCallback.

    Complete Example

    javascript
    import { memo, useCallback, useState } from "react";
    const Child = memo(function Child({ onClick }) {
    console.log("Child rendered");
    return (
    <button onClick={onClick}>
    Child Button
    </button>
    );
    });
    function Parent() {
    const [count, setCount] = useState(0);
    const handleChildClick = useCallback(() => {
    console.log("Child clicked");
    }, []);
    return (
    <div>
    <p>Count: {count}</p>
    <button
    onClick={() =>
    setCount(previous => previous + 1)
    }
    >
    Increment
    </button>
    <Child onClick={handleChildClick} />
    </div>
    );
    }
    export default Parent;

    When count changes:

    text
    Parent renders
    ↓
    count changes
    ↓
    handleChildClick remains same
    ↓
    Child receives same onClick reference
    ↓
    memo can skip Child

    useCallback Without memo

    Consider:

    javascript
    function Parent() {
    const handleClick = useCallback(() => {
    console.log("Clicked");
    }, []);
    return (
    <button onClick={handleClick}>
    Click
    </button>
    );
    }

    This is valid.

    But it may provide no meaningful benefit.

    Why?

    Because the button is not a memoized child that needs a stable callback reference.

    A new function would usually be completely fine:

    javascript
    function Parent() {
    const handleClick = () => {
    console.log("Clicked");
    };
    return (
    <button onClick={handleClick}>
    Click
    </button>
    );
    }

    Therefore:

    useCallback is not something you should automatically add to every event handler.

    useCallback Is About Referential Equality

    Suppose:

    javascript
    const first = () => {};
    const second = () => {};

    Even though they contain identical logic:

    javascript
    first === second

    is:

    text
    false

    With useCallback, React can preserve the same function reference between renders while dependencies remain unchanged.

    Conceptually:

    text
    Render 1
    ↓
    Function A
    Render 2
    ↓
    Same dependencies
    ↓
    Function A reused
    Render 3
    ↓
    Dependency changes
    ↓
    Function B created

    This stable identity can matter for:

    • memo
    • Effect dependencies
    • Custom Hooks
    • Third-party integrations
    • Subscription management

    useCallback With Dependencies

    Consider:

    javascript
    function UserProfile({ userId }) {
    const handleEdit = useCallback(() => {
    editUser(userId);
    }, [userId]);
    return (
    <button onClick={handleEdit}>
    Edit
    </button>
    );
    }

    The callback uses:

    text
    userId

    Therefore:

    javascript
    [userId]

    is a dependency.

    Mental model:

    text
    userId unchanged
    ↓
    Reuse callback
    userId changed
    ↓
    Create new callback

    Missing Dependencies

    A common mistake is:

    javascript
    const handleEdit = useCallback(() => {
    editUser(userId);
    }, []);

    The callback reads:

    javascript
    userId

    but the dependency array is empty.

    This can cause the callback to use an outdated value.

    Prefer:

    javascript
    const handleEdit = useCallback(() => {
    editUser(userId);
    }, [userId]);

    Dependencies should accurately represent the reactive values used by the callback.

    Functional State Updates and useCallback

    Sometimes a callback can avoid depending on current state by using a functional state update.

    Consider:

    javascript
    function Counter() {
    const [count, setCount] = useState(0);
    const increment = useCallback(() => {
    setCount(count + 1);
    }, [count]);
    return (
    <button onClick={increment}>
    {count}
    </button>
    );
    }

    The callback depends on:

    javascript
    count

    so its identity changes whenever count changes.

    If the update only needs the previous state, use a functional updater:

    javascript
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);

    Now the callback does not need the current count value.

    This can make its identity stable.

    Why This Pattern Is Useful

    Compare:

    javascript
    const increment = useCallback(() => {
    setCount(count + 1);
    }, [count]);

    with:

    javascript
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);

    The second version says:

    text
    I do not need the current render's count.
    I only need the previous state supplied by React.

    This can reduce dependencies and make the callback stable.

    useCallback and Event Handlers

    Most event handlers do not require useCallback.

    Simple:

    javascript
    function Form() {
    function handleSubmit(event) {
    event.preventDefault();
    }
    return (
    <form onSubmit={handleSubmit}>
    ...
    </form>
    );
    }

    There is usually no reason to memoize this function unless its reference stability matters.

    Use:

    javascript
    useCallback(...)

    when there is a concrete reason.

    useCallback With memoized Children

    This is the most common optimization pattern.

    Parent:

    javascript
    const handleDelete = useCallback(
    (id) => {
    deleteItem(id);
    },
    []
    );

    Child:

    javascript
    const TodoItem = memo(function TodoItem({
    todo,
    onDelete
    }) {
    return (
    <div>
    <span>{todo.text}</span>
    <button onClick={() => onDelete(todo.id)}>
    Delete
    </button>
    </div>
    );
    });

    The stable onDelete reference can help memo skip renders when the child's other props are unchanged.

    Important: memo and useCallback Work Together

    useCallback by itself does not stop a child from rendering.

    For example:

    javascript
    const handleClick = useCallback(
    () => {},
    []
    );
    return <Child onClick={handleClick} />;

    If Child is not optimized and its parent renders, the child can still render.

    The optimization is often:

    text
    useCallback
    +
    memo
    ↓
    Stable function prop
    ↓
    Child can skip unnecessary rendering

    Example With a Large List

    Suppose:

    javascript
    const TodoItem = memo(function TodoItem({
    todo,
    onDelete
    }) {
    return (
    <li>
    {todo.text}
    <button onClick={() => onDelete(todo.id)}>
    Delete
    </button>
    </li>
    );
    });

    Parent:

    javascript
    function TodoList({ todos }) {
    const handleDelete = useCallback((id) => {
    // delete todo
    }, []);
    return (
    <ul>
    {todos.map(todo => (
    <TodoItem
    key={todo.id}
    todo={todo}
    onDelete={handleDelete}
    />
    ))}
    </ul>
    );
    }

    A stable callback can prevent every TodoItem from receiving a new function reference when unrelated parent state changes.

    This matters more when:

    • The list is large.
    • Child rendering is expensive.
    • Parent updates frequently.

    useCallback With useEffect

    A callback can also be a dependency of an effect.

    For example:

    javascript
    function Chat({ roomId }) {
    const createOptions = useCallback(() => {
    return {
    roomId
    };
    }, [roomId]);
    useEffect(() => {
    const options = createOptions();
    connect(options);
    }, [createOptions]);
    return <div>Chat</div>;
    }

    Because createOptions is memoized, the effect can depend on a stable callback reference.

    However, this does not automatically mean useCallback is the best design.

    Often the function can simply be moved inside the effect:

    javascript
    useEffect(() => {
    const options = {
    roomId
    };
    connect(options);
    }, [roomId]);

    This is simpler.

    The important lesson is:

    Do not introduce useCallback merely to satisfy an effect dependency if the function can be structured more clearly without it.

    Avoid Unnecessary Callback Dependencies

    Consider:

    javascript
    const createOptions = useCallback(() => {
    return {
    roomId
    };
    }, [roomId]);
    useEffect(() => {
    const options = createOptions();
    connect(options);
    }, [createOptions]);

    This works, but may be unnecessarily complicated.

    Prefer:

    javascript
    useEffect(() => {
    const options = {
    roomId
    };
    connect(options);
    }, [roomId]);

    The effect now directly expresses what it synchronizes with.

    useCallback and Custom Hooks

    Custom Hooks sometimes return callback functions.

    For example:

    javascript
    function useCounter() {
    const [count, setCount] = useState(0);
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);
    return {
    count,
    increment
    };
    }

    A component can use:

    javascript
    function Counter() {
    const {
    count,
    increment
    } = useCounter();
    return (
    <button onClick={increment}>
    {count}
    </button>
    );
    }

    A stable callback can be useful when consumers depend on its identity.

    Custom Hooks will be covered later in the course.

    useCallback and Context

    Functions are sometimes passed through Context.

    For example:

    javascript
    const AuthContext = createContext(null);

    A provider might expose:

    javascript
    const login = useCallback(
    async credentials => {
    // ...
    },
    []
    );

    If a provider creates a new function on every render, consumers may observe changing context values.

    However, remember that Context performance involves the identity of the complete context value too.

    For example:

    javascript
    const value = useMemo(
    () => ({
    user,
    login
    }),
    [user, login]
    );

    This is an advanced optimization pattern and should be used when context re-render behavior actually matters.

    useCallback Does Not Make Functions Faster

    This is an important misconception.

    useCallback does not optimize the execution speed of:

    javascript
    handleClick();

    The function itself does not become computationally faster.

    Instead, useCallback can preserve its reference.

    Think:

    text
    Function execution speed
    ≠
    Function reference stability

    useCallback addresses the second.

    useCallback Does Not Prevent State Updates

    Consider:

    javascript
    const handleClick = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);

    Clicking still updates state and causes the component to render.

    useCallback does not block state updates.

    It only controls whether the callback reference is recreated.

    useCallback Is Not a Replacement for memo

    These solve different problems:

    text
    useCallback
    ↓
    Stable function reference
    memo
    ↓
    Potentially skip child render

    For callback optimization to affect a child, the child usually needs to care about prop identity, commonly through memo.

    useCallback and Closures

    A callback created during rendering closes over values from that render.

    For example:

    javascript
    function User({ name }) {
    const greet = useCallback(() => {
    console.log(name);
    }, [name]);
    // ...
    }

    The callback uses:

    text
    name

    from the render that created it.

    When name changes, the dependency changes and React can provide a new callback that closes over the new value.

    This is why dependencies matter.

    Stale Closures

    Incorrect:

    javascript
    function User({ name }) {
    const greet = useCallback(() => {
    console.log(name);
    }, []);
    return <button onClick={greet}>Greet</button>;
    }

    If name changes, the callback can retain the old value because it was memoized without name as a dependency.

    Prefer:

    javascript
    const greet = useCallback(() => {
    console.log(name);
    }, [name]);

    This keeps the callback synchronized with the value it reads.

    Dependency Arrays Should Be Honest

    Do not remove dependencies merely to keep a callback stable.

    Incorrect thinking:

    text
    "I don't want this function to change,
    so I'll use []"

    Correct thinking:

    text
    "What reactive values does this function read?"

    Then specify those dependencies.

    If the dependency list becomes inconvenient, reconsider the callback's design.

    useCallback and Objects

    Suppose:

    javascript
    const handleSave = useCallback(() => {
    save({
    name,
    email
    });
    }, [name, email]);

    This is appropriate if the callback needs the current values.

    Do not try to force:

    javascript
    []

    just to keep the function stable.

    Correctness comes first.

    useCallback and Performance

    The existence of useCallback does not mean every function should be memoized.

    Memoization has its own costs and complexity.

    For many components:

    javascript
    function Button() {
    const handleClick = () => {
    console.log("Clicked");
    };
    return <button onClick={handleClick}>Click</button>;
    }

    is perfectly good.

    Adding:

    javascript
    const handleClick = useCallback(
    () => console.log("Clicked"),
    []
    );

    may not improve anything.

    When useCallback Is Useful

    Good candidates include:

    1. Callback passed to a memoized child

    javascript
    const handleDelete = useCallback(
    id => deleteTodo(id),
    []
    );

    2. Function used as a dependency

    When a callback genuinely needs stable identity:

    javascript
    useEffect(() => {
    subscribe(handleEvent);
    return () => {
    unsubscribe(handleEvent);
    };
    }, [handleEvent]);

    3. Custom Hook API

    When a Hook returns callbacks whose stable identity benefits consumers:

    javascript
    return {
    increment: useCallback(...),
    reset: useCallback(...)
    };

    4. Expensive child trees

    When stable callback props help memoized child components avoid expensive rendering.

    When useCallback Is Not Useful

    Avoid it for:

    Simple local handlers

    javascript
    function handleClick() {
    console.log("Clicked");
    }

    Functions that are not passed anywhere important

    javascript
    function calculate() {
    return 10 + 20;
    }

    Functions passed to ordinary DOM elements

    javascript
    <button onClick={handleClick}>
    Click
    </button>

    A browser event handler does not normally need a stable React-level reference for optimization.

    Every function by default

    Do not adopt:

    text
    useCallback everywhere

    as a coding standard without a reason.

    Common useCallback Mistakes

    Mistake 1: Using useCallback Everywhere

    Avoid:

    javascript
    const handleClick = useCallback(
    () => console.log("Clicked"),
    []
    );

    when no consumer benefits from the stable identity.

    Mistake 2: Thinking useCallback Caches Results

    Incorrect:

    javascript
    const total = useCallback(
    () => calculateTotal(items),
    [items]
    );

    This gives you a function, not the calculated total.

    Use:

    javascript
    const total = useMemo(
    () => calculateTotal(items),
    [items]
    );

    Mistake 3: Missing Dependencies

    Incorrect:

    javascript
    const handleSave = useCallback(() => {
    save(user);
    }, []);

    If user is reactive, it belongs in the dependency list:

    javascript
    const handleSave = useCallback(() => {
    save(user);
    }, [user]);

    Mistake 4: Using useCallback to Hide an Architectural Problem

    If a component is rendering too often, blindly memoizing every callback may hide the actual problem.

    Investigate:

    • State placement
    • Component boundaries
    • Context usage
    • Expensive calculations
    • Large lists
    • Unnecessary parent renders

    Mistake 5: Forgetting memoized Children

    This:

    javascript
    const handleClick = useCallback(...);

    does not automatically stop a child from rendering.

    The child may need:

    javascript
    const Child = memo(...);

    for stable callback props to contribute to render skipping.

    Mistake 6: Optimizing Before Measuring

    Do not assume that every new function is a performance problem.

    Use profiling and real application behavior to identify actual bottlenecks.

    Practical Example: Todo Application

    javascript
    import { memo, useCallback, useState } from "react";
    const TodoItem = memo(function TodoItem({
    todo,
    onDelete
    }) {
    console.log("Rendering:", todo.id);
    return (
    <li>
    {todo.text}
    <button onClick={() => onDelete(todo.id)}>
    Delete
    </button>
    </li>
    );
    });
    function TodoList() {
    const [todos, setTodos] = useState([
    { id: 1, text: "Learn React" },
    { id: 2, text: "Learn Hooks" }
    ]);
    const [search, setSearch] = useState("");
    const handleDelete = useCallback((id) => {
    setTodos(previousTodos =>
    previousTodos.filter(todo => todo.id !== id)
    );
    }, []);
    const visibleTodos = todos.filter(todo =>
    todo.text
    .toLowerCase()
    .includes(search.toLowerCase())
    );
    return (
    <div>
    <input
    value={search}
    onChange={event =>
    setSearch(event.target.value)
    }
    placeholder="Search"
    />
    <ul>
    {visibleTodos.map(todo => (
    <TodoItem
    key={todo.id}
    todo={todo}
    onDelete={handleDelete}
    />
    ))}
    </ul>
    </div>
    );
    }
    export default TodoList;

    Here:

    text
    handleDelete
    ↓
    useCallback
    ↓
    stable function reference
    ↓
    TodoItem receives same onDelete reference
    ↓
    memo can optimize rendering

    The list filtering itself is not memoized in this example because whether it needs memoization depends on the size and cost of the actual data.

    Practical Example: Callback With State

    javascript
    function Counter() {
    const [count, setCount] = useState(0);
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);
    return (
    <button onClick={increment}>
    Count: {count}
    </button>
    );
    }

    The callback has no dependency on count because it uses:

    javascript
    previous => previous + 1

    instead.

    This demonstrates a useful combination:

    text
    useCallback
    +
    functional state updater
    ↓
    stable callback

    Practical Example: Search Callback

    javascript
    function Search({ onSearch }) {
    const [query, setQuery] = useState("");
    const handleSearch = useCallback(() => {
    onSearch(query);
    }, [onSearch, query]);
    return (
    <div>
    <input
    value={query}
    onChange={event =>
    setQuery(event.target.value)
    }
    />
    <button onClick={handleSearch}>
    Search
    </button>
    </div>
    );
    }

    The callback depends on:

    text
    onSearch
    query

    Therefore:

    javascript
    [onSearch, query]

    is the correct dependency list.

    Practical Example: Custom Hook

    javascript
    import { useCallback, useState } from "react";
    function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);
    const decrement = useCallback(() => {
    setCount(previous => previous - 1);
    }, []);
    const reset = useCallback(() => {
    setCount(initialValue);
    }, [initialValue]);
    return {
    count,
    increment,
    decrement,
    reset
    };
    }

    Usage:

    javascript
    function Counter() {
    const {
    count,
    increment,
    decrement,
    reset
    } = useCounter(10);
    return (
    <div>
    <p>{count}</p>
    <button onClick={increment}>
    +
    </button>
    <button onClick={decrement}>
    -
    </button>
    <button onClick={reset}>
    Reset
    </button>
    </div>
    );
    }

    This demonstrates how useCallback can be part of a reusable Hook API.

    Debugging useCallback

    If a callback changes unexpectedly, check:

    1. Dependencies

    javascript
    useCallback(fn, [dependency])

    Does one of those dependencies change?

    2. Object dependencies

    If:

    javascript
    const options = {};

    is recreated every render, a callback depending on it can also change.

    3. Parent rendering

    A callback may be recreated because its dependencies changed due to state or props.

    4. Is the callback actually causing a problem?

    A changing function reference is not automatically a bug.

    5. Is the child memoized?

    If the goal is child render optimization, verify that the child uses an appropriate memoization strategy.

    A Practical Optimization Checklist

    Before adding useCallback, ask:

    text
    Is this function passed to a memoized child?
    │
    ┌─────┴─────┐
    │ │
    Yes No
    │ │
    ▼ ▼
    Could stable Usually
    identity reduce no need
    child renders?

    Also ask:

    text
    Is this function used as a Hook dependency?
    │
    Yes
    ↓
    Would stable identity improve synchronization?
    │
    Yes
    ↓
    Consider
    useCallback

    And finally:

    text
    Is there a measured performance problem?
    │
    No
    ↓
    Avoid unnecessary memoization

    useCallback and Component Design

    Sometimes the best optimization is not memoization.

    For example, instead of:

    text
    Large Parent
    ↓
    Everything re-renders
    ↓
    Many useCallbacks
    ↓
    Many memoized children

    a better architecture may be:

    text
    Parent
    ├── Frequently changing component
    └── Stable component

    Good component boundaries can naturally reduce unnecessary work.

    Use memoization as a targeted optimization rather than as a substitute for component design.

    useCallback and Referential Stability

    Referential stability means keeping the same reference between renders when the relevant dependencies have not changed.

    For a callback:

    javascript
    const handleClick = useCallback(
    () => doSomething(),
    []
    );

    the reference can remain stable.

    This matters when code compares:

    javascript
    previousCallback === currentCallback

    or when React's memoization mechanisms use reference equality.

    The important distinction is:

    text
    Same behavior
    ≠
    Same reference

    useCallback is about the second.

    useCallback and Memory

    Memoization is not free.

    React needs to retain the callback and track its dependencies.

    Therefore:

    text
    No performance problem
    ↓
    useCallback everywhere
    ↓
    More complexity
    ↓
    Potentially little or no benefit

    Prefer targeted optimization.

    Best Practices

    1. Use useCallback for a Reason

    A stable callback should solve an actual identity or performance problem.

    2. Keep Dependencies Accurate

    Do not remove dependencies just to preserve function identity.

    3. Use Functional State Updates When Appropriate

    This can reduce callback dependencies:

    javascript
    setCount(previous => previous + 1);

    4. Combine With memoized Children When Appropriate

    A stable callback is most useful for child optimization when the child can actually skip renders.

    5. Do Not Confuse Function Identity With Execution Speed

    useCallback does not make the function itself faster.

    6. Prefer Simpler Code

    If a normal function works well:

    javascript
    const handleClick = () => {};

    do not add useCallback without a reason.

    7. Profile Performance

    Use React DevTools Profiler and browser tools when investigating actual rendering bottlenecks.

    Interview questions & answers

    Interview preparation

    10 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    4
    1. 1What is useCallback?
      Beginner

      Model answer

      useCallback is a React Hook that caches a function definition between renders until its dependencies change.
    2. 2What is the difference between useMemo and useCallback?
      Beginner

      Model answer

      useMemo caches a calculated value. useCallback caches a function reference.
    3. 3Why are dependencies important?
      Beginner

      Model answer

      They ensure the callback sees current reactive values rather than stale values.

    4. 4Is useCallback always necessary for event handlers?
      Beginner

      Model answer

      No.

      Most event handlers do not need memoization.

      Use it when stable function identity provides a concrete benefit.

    Intermediate

    3
    1. 5Why use useCallback?
      Intermediate

      Model answer

      It can provide a stable function reference, which can be useful for memoized child components, Hook dependencies, custom Hook APIs, and certain integrations.

    2. 6Does useCallback prevent re-renders?
      Intermediate

      Model answer

      No.

      It does not prevent the component that owns the callback from rendering.

      It can help a memoized child skip rendering when the callback prop remains referentially equal.

    3. 7Can useCallback be used without React.memo?
      Intermediate

      Model answer

      Yes, but its performance benefit is often limited unless another part of the system benefits from stable function identity.

    Advanced

    3
    1. 8Does useCallback make a function execute faster?
      Advanced

      Model answer

      No.

      It preserves the function reference; it does not optimize the function's internal execution.

    2. 9What happens when a dependency changes?
      Advanced

      Model answer

      React can provide a new callback function reflecting the new dependency values.

    3. 10What is a stale closure?
      Advanced

      Model answer

      A stale closure occurs when a function retains outdated values from an earlier render because the function was not correctly synchronized with the values it reads.

    Practice Exercises

    Exercise 1: Memoized Child

    Create:

    text
    Parent
    ↓
    Memoized Child

    Pass a callback from the parent.

    First implement it without useCallback.

    Then add useCallback and observe the child's render behavior.

    Exercise 2: Counter Callback

    Create a counter where:

    • The increment callback uses useCallback.
    • The callback uses a functional state updater.
    • The dependency array remains empty.

    Explain why count does not need to be a dependency.

    Exercise 3: Todo List

    Create:

    • Todo list
    • Memoized Todo item
    • Delete callback
    • Search state

    Use useCallback for the delete handler.

    Observe which components render when the search term changes.

    Exercise 4: Callback Dependencies

    Create a callback that uses:

    javascript
    userId
    theme

    Determine the correct dependency array.

    Exercise 5: Stale Closure

    Create an example where a callback incorrectly omits a dependency.

    Observe the stale value.

    Then fix it by adding the correct dependency.

    Exercise 6: Remove Unnecessary useCallback

    Given:

    javascript
    const handleClick = useCallback(() => {
    console.log("Clicked");
    }, []);

    Determine whether the memoization provides a meaningful benefit.

    If not, replace it with a normal function.

    Mini Project: Optimized Todo Manager

    Build a Todo Manager with:

    text
    Add Todo
    Delete Todo
    Search Todo
    Toggle completed
    Theme toggle

    Use:

    text
    useState
    useCallback
    memo

    Suggested architecture:

    text
    TodoManager
    │
    ├── Search
    ├── ThemeToggle
    └── TodoList
    │
    ├── TodoItem
    ├── TodoItem
    └── TodoItem

    Requirements:

    1. Store todos with useState.
    2. Store search with useState.
    3. Create stable delete callback with useCallback.
    4. Create stable toggle callback with useCallback.
    5. Memoize TodoItem where appropriate.
    6. Use functional state updates.
    7. Observe render behavior with console logging.
    8. Avoid memoizing callbacks that provide no meaningful benefit.

    Example:

    javascript
    const TodoItem = memo(function TodoItem({
    todo,
    onDelete,
    onToggle
    }) {
    return (
    <li>
    <input
    type="checkbox"
    checked={todo.completed}
    onChange={() => onToggle(todo.id)}
    />
    <span>{todo.text}</span>
    <button onClick={() => onDelete(todo.id)}>
    Delete
    </button>
    </li>
    );
    });
    function TodoManager() {
    const [todos, setTodos] = useState([]);
    const [search, setSearch] = useState("");
    const handleDelete = useCallback((id) => {
    setTodos(previousTodos =>
    previousTodos.filter(todo => todo.id !== id)
    );
    }, []);
    const handleToggle = useCallback((id) => {
    setTodos(previousTodos =>
    previousTodos.map(todo =>
    todo.id === id
    ? {
    ...todo,
    completed: !todo.completed
    }
    : todo
    )
    );
    }, []);
    const visibleTodos = todos.filter(todo =>
    todo.text
    .toLowerCase()
    .includes(search.toLowerCase())
    );
    return (
    <div>
    <input
    value={search}
    onChange={event =>
    setSearch(event.target.value)
    }
    placeholder="Search todos"
    />
    <ul>
    {visibleTodos.map(todo => (
    <TodoItem
    key={todo.id}
    todo={todo}
    onDelete={handleDelete}
    onToggle={handleToggle}
    />
    ))}
    </ul>
    </div>
    );
    }

    The important optimization relationship is:

    text
    useCallback
    ↓
    Stable callbacks
    ↓
    memoized TodoItem
    ↓
    Potentially fewer child renders

    Quick Cheat Sheet

    Import

    javascript
    import { useCallback } from "react";

    Basic callback

    javascript
    const handleClick = useCallback(
    () => {
    console.log("Clicked");
    },
    []
    );

    Callback with dependency

    javascript
    const handleEdit = useCallback(
    () => {
    editUser(userId);
    },
    [userId]
    );

    Functional state update

    javascript
    const increment = useCallback(() => {
    setCount(previous => previous + 1);
    }, []);

    With memoized child

    javascript
    const Child = memo(function Child({
    onClick
    }) {
    return <button onClick={onClick}>Click</button>;
    });

    Core distinction

    text
    useMemo
    → memoize a value
    useCallback
    → memoize a function reference
    memo
    → potentially skip component rendering

    Remember

    text
    useCallback does NOT:
    ✗ cache function results
    ✗ make function execution faster
    ✗ prevent parent renders
    ✗ make every callback necessary
    useCallback CAN:
    ✓ preserve function identity
    ✓ help memoized children
    ✓ stabilize Hook dependencies
    ✓ support custom Hook APIs

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.