The two Hooks have the same signature and the same dependency rules, and they differ in one thing: what gets cached. useMemo runs your function and caches what it returned. useCallback caches the function itself and never runs it. That makes useCallback(fn, deps) nothing more than shorthand:
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]); // exactly the same thingThe double arrow is the whole story: useMemo needs a function that returns the function you want to keep. useCallback exists because that shape is easy to mistype and hard to read.
| useMemo | useCallback | |
|---|---|---|
| What is cached | The return value | The function object |
| Is your function called? | Yes, when a dependency changed | Never by the Hook |
| Typical use | Derived data, filtered arrays, JSX | Event handlers, Effect dependencies |
| Returns on a cache hit | Last computed value | Last function, same address |
The mistake this produces is writing useCallback(() => expensiveTotal(rows), [rows]) and expecting the total to be cached. It is not: you cached the wrapper, and expensiveTotal still runs in full whenever something calls it. Anything whose result you want to reuse belongs in useMemo — which can also do everything useCallback does, including keeping a whole object of handlers stable for a context value (Context Performance Pitfalls).