The usage of useCallback
is something very controversial where it exists two groups of person:
- those who memoize everything
- those who only memoize the strict necessary
In this article I expose you my rules which makes me decide to use useCallback
.
useCallback
is a native hook provided by React, it permits to give you a memoized callback.
As a quick reminder when developing with functional component, the body of the function is the render.
So if I define a function inside the component (render), this function will be redefined at every render giving you a new references.
function myComponent() {
// At each renders I am redefined
// I.E. I will have a new references
const onClick = () => {
console.log('I have been clicked');
};
return <button onClick={onClick}>Click me</button>;
}
My answer is simply NO.
Most of the time we don’t care. This is not a problem for our javascript engine to do it, it’s fast and no memory issue with that.
So when do we care? Let me just a second I want you to show a quick implementation of the hook before :)
The logic is pretty simple when you know how to implement some memoization in JS. If it’s not the case you can read my article :)
But in the case of React there is no closure.
Th previous callback and dependencies are stored in the Fiber node of the component. This is stored within the key memoizedState
.
In the next code template, I show you an implementation example:
import shallowEqual from './shallowEqual';
// Things stored in the React element
const memoizedState = {
lastCallback: undefined,
lastDependencies: undefined,
};
// In reality there is multiple implementation of
// it behind the hood
// But it's a simplified example
function useCallback(callback, dependencies) {
if (
!shallowEqual(
memoizedState.lastDependencies,
dependencies,
)
) {
memoizedState.lastCallback = callback;
memoizedState.lastDependencies = dependencies;
}
return memoizedState.lastCallback;
}
As you can see a shallowEqual
is used to compare the dependencies. If you want to know more about the different types of equality, do not hesitate to read my article about it .
And now let’s see with a quick gif how to see this in a browser:
As usual, I will begin by told not to do premature optimization. Only do this when you have real performance problem in your application / component library.
For example if you have a component in your code base which has slow renders and that most of the time they can be prevented because it doesn’t need to be re-render (no props change in reality).
In this case we will memo the component. And from here it’s important that references do not change unnecessarily.
Now imagine that this component is a Button
. Yeah it would probably not happen for a button, I know. But it’s just an example ;)
So in this case it will be important that the onClick
callback has a stable reference.
import React, { useCallback } from 'react';
function App() {
const onClick = useCallback(() => {
// Doing some stuff here
}, []);
return (
<MemoizedButton onClick={onClick}>
Click me
</MemoizedButton>
);
}
function Button({ onClick }, children) {
// Doing some heavy process here
return <button onClick={onClick}>{children}</button>;
}
const MemoizedButton = React.memo(Button);
And the reciprocal is also true. If you useCallback
but do not React.memo
the Button
then instead you make your performance worse.
Why? Because as we have seen at each render there is 2 callbacks that are in memory. Yep it’s not dramatic, but by doing this, I find the codebase less readable.
Another reason which makes me useCallback
is when I need to put the callback in the dependency of useEffect
, useLayoutEffect
or useCallback
.
import { useCallback, useEffect, useState } from 'react';
import apiCall from './apiCall';
function App() {
const [data, setData] = useState();
const fetchData = useCallback(() => {
apiCall().then(setData);
}, []);
useEffect(() => {
// We fetch the data at mounting
fetchData();
}, [fetchData]);
return (
<div>
<p>The data is: {data}</p>
<button onClick={fetchData}>Refetch data</button>
</div>
);
}
If it was used only in the useEffect
, I would have defined the method directly in it:
useEffect(() => {
const fetchData = () => {
apiCall().then(setData);
};
// We only fetch the data at mounting
fetchData();
}, [fetchData]);
Another will be when I do some “public” hook, for example in a library, or a generic hook that could be used in multiple place. Then I will stabilize returned callbacks.
Why do I do this?
The reason is that I don’t know where it will be used. It could be:
- in a useEffect / useCallback / useLayoutEffect then it will be required to have a stable reference
- in an event handler, then it’s not required at all
So to satisfy both cases, I provide a stable reference :)
import { useCallback } from 'react';
export function usePublicHook() {
return useCallback(() => {
console.log("It's only an example");
}, []);
}
But if I do a hook just to extract a specific logic from a component (for testing purpose and to malke the component easier), and it can’t be used in another one. Then I will only useCallback
id it’s necessary because I know the use case.
And here we go. That’s how I use the hook useCallback
, hoping that it can help you to have a better code base, because it makes the code more complicated to read.
To summarize:
- if I have performances issues
- if I used it as dependency of another hook (
useEffect
,useLayoutEffect
,useCallback
, …) - when I do a public / generic hook
I hope to see React Forget released as soon as possible (yep I’m dreaming), which will help us to stop wondering :) If you don’t know what is React Forget, let’s check this video .
Do you use useCallback
in another use case? If it’s the case do not hesitate to put it in comment.
You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.