Menu

React useCallback Hook: When It Actually Helps

useCallback returns the same function between renders until its dependencies change. It only helps when that function is passed to a memo child or used as an effect dependency. Learn the syntax, the difference from useMemo, and the mistakes to avoid.

This page includes runnable editors - edit, run, and see output instantly.

useCallback caches a function between renders: it returns the same function object until one of its dependencies changes. That matters only when something compares the function by reference, which in practice means a child wrapped in memo or an effect that lists the function as a dependency.

Both buttons are memoized and both do the same thing. Toggle the theme and watch the Console: only render Inline function appears, because saveInline is a new function on every render, so memo sees a changed prop. saveCached is the same function each time, so that button is skipped.

What it caches

const cachedFn = useCallback(fn, dependencies);
  • fn is the function you want to keep. React does not call it; it returns it to you.
  • dependencies lists every value from the component that fn reads. On each render React compares them with the previous ones using Object.is.
  • If nothing changed, you get the function from the previous render. If something changed, you get the function you passed this time, and React keeps that one for next time.

Note what this means: the arrow function inside useCallback is still created on every render, because it is just an argument. useCallback does not save that work. Its only job is to hand back an older, identical function so the reference stays the same.

useCallback vs useMemo

They are the same mechanism. useMemo caches the result of calling a function, useCallback caches the function itself:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Use useCallback for functions because it reads better, and useMemo for computed values such as a filtered list or an options object.

Dropping dependencies with updater functions

A cached function must list every state value it reads, and every time that state changes, the function changes too. For a list of memoized rows that is a problem: a remove function that reads todos changes whenever todos changes, so every remaining row renders again after each removal. Use the updater form of the setter so the function no longer reads todos at all.

Remove a row: the Console stays quiet, because the other rows got the same todo object and the same remove function as before. Change the function to useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) and try again: now every remaining row logs a render after each removal.

Functions as effect dependencies

An effect that calls a function declared in the component has to list that function as a dependency. If the function is new on every render, the effect runs after every render. useCallback keeps it stable until its own inputs change:

Typing a message does not reconnect. Switching rooms logs a disconnect from the old room and a connect to the new one. Delete the useCallback wrapper (keep the arrow function) and each keystroke disconnects and reconnects.

There is a simpler fix here, though: move the function inside the effect. Then the effect depends on roomId directly and no function needs to be stable.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Prefer that when the function is only used by the effect. Keep useCallback for functions you also pass down or return from a custom hook, where callers might put them in their own dependency arrays.

Checking whether a function changed

When a memo child keeps rendering and you are not sure why, check whether the function prop is really the same object between renders. Keep the previous one in a ref and compare:

Click Count: the Console says increment changed: false, because step did not change. Click Step and it says true: step is a dependency, so React handed back the new function. A function that changes on every render usually means a dependency is itself new on every render, such as an object or another function declared in the component.

useCallback in custom hooks

A custom hook that returns functions is a good place for useCallback even when you do not know who will call it. The component using the hook may pass those functions to a memo child or list them in an effect, and it cannot make them stable from the outside. Wrapping them inside the hook gives every caller a stable reference by default:

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

The common mistake: wrapping everything

useCallback around a handler that goes straight to a <button> or <input> does nothing useful. React skips no work for a DOM element because its handler stayed the same, and a component that is not wrapped in memo renders anyway when its parent does.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Each extra useCallback adds a dependency array that someone can get wrong, and a stale dependency is a real bug (the function keeps reading old state). Add it when you have a memo child or a dependency array on the receiving end, not by default.

React Compiler 1.0, when your build uses it, memoizes functions and values automatically, which removes the need for most hand written useCallback calls. Without the compiler, the rules above still apply, and manual useCallback is still common in existing code.

Frequently Asked Questions

What does useCallback do?

It returns the same function object on every render until one of its dependencies changes. It does not stop the function from being created; it hands back the function from an earlier render instead of the new one.

When should I use useCallback?

When you pass a function to a child wrapped in memo, or when the function is a dependency of an effect or of another hook. In both cases a new function on every render would defeat the optimization or rerun the effect.

What is the difference between useCallback and useMemo?

useCallback(fn, deps) caches fn itself. useMemo(() => value, deps) caches the value a function returns. useCallback(fn, deps) is exactly useMemo(() => fn, deps).

Should I wrap every function in useCallback?

No. If the function goes to a plain DOM element or a component that is not memoized, a stable reference changes nothing, and the extra dependency arrays make the code harder to read and easier to get wrong.

Does useCallback make my component faster?

Not by itself. It only helps when something downstream compares the function by reference, such as memo or an effect's dependency array.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED