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:
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.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.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);
}, []);
}