Well, not exactly, but it may not work quite how you expect. It didn't work how I expected.
Functions Not Required
The useCallback docs provide a couple of interesting clues (emphasis mine). It's a big block of text, but it's all relevant.
fn
: The function value that you want to cache. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On next renders, React will give you the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call your function. The function is returned to you so you can decide when and whether to call it.
Let's break that down:
-
useCallback
receives a (function) value and a dependency array. - It caches and returns that (function) value.
- Whenever the calling function is re-run it returns the original (function) value unless the dependencies array changes.
- Whenever the dependencies change, it will store and return that new (function) value.
Hooks, Closures, and Stale Values
This is really important for functions in React. Functions have access to the scope in which they were created (closures!), including the values of props and other hooks. Most hooks return immutable values, so if the props or other hooks change, the closure in which the callback was created becomes stale. It will still have old values if we call the function. We need to let go of the copy with a stale closure and get a fresh one.
Call Me, Maybe?
The docs say twice that useCallback
doesn't call our function. It just holds onto it for us so we can call it later if we ever need to. And the dependencies are there in case our phone number changes scope changes and we need a fresh version with fresh scope.
But since React never calls the function, it doesn't matter if it's a function! React is just storing a value. We've just agreed by naming convention that value will be a function. But it doesn't have to be.
First Class Functions FTW
JavaScript lets us store functions as values and pass them as arguments. That's exactly what this higher-order useCallback
function expects us to do. But we can pass a number, an object, a Promise, or anything.
// No dependencies, so it never changes
const something = useCallback([1, '2', 'c', new Date('4')], []);
const another = useRef(something);
something === another.current; // true
Cough SIGNALS Cough
Incidentally, signals return a function rather than a value, where the function is a consistent return and the signal manages the scope internally rather than the component. This is why signals rule don't require manual dependency tracking and do not experience stale values.
Just sayin'.
Simpler useEffect
?
If you are hanging onto a simple object or computation, it might look like this:
// useEffect & useState Edition
const MyComponent = ({ input, color, shape }) => {
const [state, setState] = useState();
useEffect(() => {
setState({ input, shape });
}, [input, shape]);
return (<Nested ...state />);
};
Or, probably better if you use useMemo
for this.
// useMemo Edition
const MyComponent = ({ input, color, shape }) => {
const state = useMemo(
() => ({input, shape }),
[input, shape],
);
return (<Nested ...state />);
};
But you could just pass that directly to useCallback
.
// useCallback Edition
const MyComponent = ({ input, color, shape }) => {
const state = useCallback({ input, shape }, [input, shape]);
return (<Nested ...state />);
};
const useMemoValue = useCallback;
What we really have here is a function that accepts a value and will hold onto it until there is a change in the dependencies array. No types. No function executions. Nothing. Just holding onto some value.
You could even use this as a slightly sketchy useRef
alternative, where you can keep and mutate an object until a dependency forces you to update it. If you have a useRef
that gets updated in a useEffect
, you might be able to merge those two operations with a misused useCallback
.
But you probably shouldn't.
You Still Want useMemo
Except in rare cases like the example above where the operation is trivial, the difference between misusing useCallback
and correctly using useMemo
is that useMemo
doesn't do any of the work in the function you pass it until the props change. Whatever is passed to useCallback
, it will do that work every time it renders the component. With useMemo
the work is creating a function, not executing the contents of that function.
Simple things like restructuring a single object in our example work well with useCallback
, but processing an array is a bad choice.
// Only executes the `.map()` when the dependencies change
const good = useMemo(
() => bigData.map(entry => complexTransform(entry, someState),
[ bigData, someState ],
);
// Runs the `.map()` every time the component is rendered
const questionable = useCallback(
bigData.map(entry => complexTransform(entry, someState),
[ bigData, someState ],
);
To be honest, trying to use useCallback
this way is probably a bad choice in general.
Convention is Communication
As the code author, you might understand that useCallback
doesn't have to store a callback function and has this "off label" use. However, other developers may not. Even future you would likely pause at that code and wonder if you meant to use a different hook. You would probably end up adding a comment to clarify the usage, and justify your decision to other developers or to your future self.
All of that means that anyone reading that code will have to stop whatever analysis they are doing to read the comment and understand why this obviously wrong code is not actually wrong and isn't part of the problem they're looking for...unless of course it is, because in doing something non-standard we created some other unexpected defect.
So you really do want useMemo
. It does what we expect. It does what the next developer expects. It doesn't require extra mental overhead to understand or explain itself.
Consistency provides a lot of benefit. When we decide to write "interesting" or clever code, it comes at the cost of maintainability and future efficiency. Sometimes it's worth it, but often it isn't.
Summary
So, useCallback
is a lie. It's not about the callback; it's about the closure. It's good to know this is the case, because it will help you understand why omitting dependencies can cause problems, but it's also good to know not to use it in this way unless you have a really good reason to do so.
Top comments (0)