5

TL;DR

Why is useCallback defined as (roughly)

function useCallback(callback, deps) {
  return useMemo((...args) => {
    return callback(...args);
  }, deps);
}

instead of like this?

function useCallback(callback) {
  const fn = useRef(callback);
  fn.current = callback;

  return useMemo((...args) => {
    return fn.current(...args);
  }, []);
}

It seems like it would solve unnecessary rerenders, while always working with the most recent version of the function. I also heard that Vue 3 optimizes in this exact same way using the cacheHandlers option.


Contextually explained version

When writing react components/hooks/contexts, you can either just write functions directly:

const bootstrapAuth = async () => {
  // ...
};

…or optimize for minimal rerenders using useCallback:

const bootstrapAuth = useCallback(async () => {
  // ...
}, []);

Myself I tend to use useCallback often, but as a teacher, I don't teach my students this from the start. It’s an extra complication, and is as far as. I know officially considered only a performance concern. It’s just useMemo, nothing more.

But when you start combining effects and functions, it can become critical, such as in:

const bootstrapAuth = async () => {
  // ...
};
useEffect(() => {
  bootstrapAuth();
}, []);

^^ this is technically incorrect (e.g. the linter will complain), but then, putting bootstrapAuth in the dependencies array is even worse, because it will rerun on every render. There seem to be three solutions:

  1. Use useCallback. But that violates the principle that it’s just a performance concern, and I don’t see how this is not a widespread problem in the react community. I usually choose this approach.

  2. Make bootstrapAuth idempotent, i.e. running it more often than necessary doesn’t cause additional effects. Making functions idempotent is always smart, but it seems like a weird band-aid given that it totally defies the effect hook. Surely this isn’t the general solution.

  3. Use a ref, like so (although the example is a bit contrived):

    const bootstrapAuthLatest = useRef(bootstrapAuth);
    bootstrapAuthLatest.current = bootstrapAuth;
    
    useEffect(() => {
      bootstrapAuthLatest.current();
    }, []);
    

This last “trick”, although a bit contrived here, does seem to be a go-to approach in helper hook libraries, and I’ve been using it a lot myself, pretty much whenever I introduce a hook that accepts a callback. And it makes you wonder, why didn’t the React team just define useCallback like so, in the very first place??

function useCallback(callback) {
  const fn = useRef(callback);
  fn.current = callback;

  return useMemo((...args) => {
    return fn.current(...args);
  }, []);
}
Kelley van Evert
  • 1,063
  • 2
  • 9
  • 17
  • does [this article](https://overreacted.io/a-complete-guide-to-useeffect/#swimming-against-the-tide) answers your question? – Yousaf Jun 27 '20 at 08:55
  • I'm not really sure, but it does cut to the heart of the problem. Dan often likes to stress the "semantic correctness" of the approach, and how it eliminates bugs etc. My question isn't really about the correctness, but about this tension between correctness and: optimizing for rerenders (if necessary) and/or single-occurrence effects on _initial prop values_. – Kelley van Evert Jun 27 '20 at 10:58
  • Dan essentially would say: "We implemented `useCallback` is the most semantically correct way, to avoid possibilities for bugs. Don't 'reach ahead' to future props if possible." My problem is that it seems super awkward. If you want to run an effect _once, on initial prop values_, then semantically, you'd have to first codify "those initial values" with refs, and then write your function in a `useEffect(..., [])`. Maybe that's Dan's preferred solution, then. – Kelley van Evert Jun 27 '20 at 11:00
  • As for optimizing for rerenders, it's simply very much discouraged by the hooks api to write a "instance-like" callback functions that operate on the instance's most recent state/props, and this in turn makes rerender-optimizing a harder task. Of course, Dan would then be right in saying that the harder task now faced is a task of semantic correctness. – Kelley van Evert Jun 27 '20 at 11:04
  • This quote by Dan cuts right to the heart of it imo: "Conceptually, you can imagine effects are a part of the render result." (my edit: effects and callbacks) – Kelley van Evert Jun 27 '20 at 11:12

1 Answers1

-1

Myself I tend to use useCallback wherever, [...]

const bootstrapAuth = useCallback(async () => {
  // ...
}, []);

is the same as

const bootstrapAuth = async () => {
  // ...
};
const bootstrapAuthCallBack = useCallback(bootstrapAuth, [])

You effectively created an additional function (compared to not using useCallback), an array, allocated the respective memory and have useCallback set its properties and run through logical expressions. Wapping everything inside useCallback does not make all code run faster by default.

hotpink
  • 2,882
  • 1
  • 12
  • 15
  • I know that. The reason for wrapping a function with `useCallback` is for referential stability when passing it as a prop to a child component, so that that child component can be optimized to exclude unnecessary rerenders, e.g. by using `React.memo` or `componentShouldUpdate`. The question isn't why/whether/when to do this, but rather why `useCallback` is implemented in the way it is, and why the "ref version" wasn't chosen instead. – Kelley van Evert Jun 27 '20 at 10:38
  • Well, then don't say you put it everywhere when you actually only apply it when its useful. – hotpink Jun 27 '20 at 11:05
  • Ok, ok, sure, I'll edit that :) I changed "wherever" to "often". (Truth is, I'm working on a React Native app now, and then rerender optimizations turn out to be quite essential, so I actually pretty much _do_ mean "wherever". For the web app version of the same app, I almost never use it.) – Kelley van Evert Jun 27 '20 at 11:06