React useCallback explained
Bart Kooijman / 2021-09-02
6 min read
The moment React introduced hooks you got some new tools in your toolbag. One of those tools is the useCallback hook. If you are confused by this hook, don't know when or why to use it, remember, you're definitely not alone. This hook regularly leads to a lot of confusion.
While explaining code, I like to visualize the process or create a mental model of the concept. I hope this blog will help you in understanding when and why you should use useCallback(). Let's go!
Performance
The useCallback hook is related to performance optimization. Luckily React is fast, so if you have a perfectly working app, you'll probably don't even need useCallback. React's performance is related to the process of deciding whether a component should rerender or not. Part of this process is comparing the previous props with the new props.
Javascript Equality
To understand the need for useCallback we first need to understand equality in Javascript. So let's take one step back. As you might know, a React component rerenders if the new props differ from the old props. How does this work? Time to practice!
Primitives
Try to examine the statements below:
1 == 1; // true
1 == true; // true
1 === true; // false
'1' == true; // true
'1' === true; // false
'true' === true; // false
true == 'true'; // false
Try to explain yourself why the statements above have the result as commented out in true or false. If you are wondering why the statements are true or false, I recommend reading this before moving on.
Objects
Try the same, but now we compare objects:
var tesla = { type: 'car' };
var ford = { type: 'car' };
ford == tesla; // false
ford === tesla; // false
tesla.type === ford.type; // true
Now it is getting interesting. Although the objects seem equal, the comparer returns false. This is because objects are compared by reference instead of values. The references are different so the comparer returns false. If we compare the properties we are comparing a string value. These kinds of values are compared by value. And it returns true.
Functions
One more, let's compare functions.
var func1 = () => 'nice' + ' car';
var func2 = () => 'nice' + ' car';
func1 == func2; // false
func1 === func2; // false
func1() == func2(); // true
func1() === func2(); // true
console.log(func1()); // "nice car"
console.log(func2()); // "nice car"
typeof func1 == typeof func2; // true
typeof func1 === typeof func2; // true
typeof func1; // "function"
Alright! Do you see the difference between comparing the function itself and comparing the results of the function? Let's log the comparison to the console:
console.log(func1); // () => "nice" + " car"
console.log(func2); // () => "nice" + " car"
console.log(func1()); // "nice car"
console.log(func2()); // "nice car"
typeof func1(); // "string"
typeof func2(); // "string"
So when we compare the results of the function we actually compare strings. These are equal, so it's true, as expected. But if we compare the function itself it returns false! Why?
func1 == func2; // false
func1 === func2; // false
typeof func1; // "function"
typeof func2; // "function"
It returns false because a function is an object, So it's compared by reference!
useCallback
Now it's time to talk about useCallback. Assume you have a component that's static. You've decided to wrap it in a React.memo. So it only rerenders if the props change. But you notice the component still rerenders, how is that possible? If you have this kind of situation, examine the type of the props. If one of these props is a function, it's time to try out useCallback!
A counter is not a good candidate for using React.memo, but for demo purposes, we will be using it here. The CounterParent component below has an input field and a child named Counter. Ideally, the counter won't rerender when we are interacting with the input field. The CounterComponent is wrapped in React.memo because it doesn't need to rerender if the props don't change.
const CounterParent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState();
const increaseCounter = () => setCount(count + 1);
return (
<>
<form>
<input
type="text"
id="name"
placeholder="Typing here will trigger a render :("
onChange={(event) => setName(event.currentTarget.value)}
/>
</form>
<Counter currentCount={count} clickHandler={increaseCounter} />
</>
);
};
export default CounterParent;
const Counter = ({ currentCount, clickHandler }) => {
const renderCount = useRef(0);
renderCount.current += 1;
return (
<>
<div>
<span>Click the button to increase the counter.</span>
<button onClick={clickHandler}>
Increment!
</button>
</div>
<div>
Current count: <strong>{currentCount}</strong>
</div>
<div>
Number of counter rerenders: <strong>{renderCount.current}</strong>
</div>
</>
);
};
export default React.memo(Counter);
Without useCallback, you'll notice that typing in the input field below will trigger an unnecessary rerender on the counter:
So let's fix this. In the component above let's implement useCallback:
// change this line:
const increaseCounter = () => setCount(count + 1);
// into this:
const increaseCounterWithCallback = useCallback(() => {
setCount(count + 1);
}, [count]);
With useCallback, the reference to the clickHandler function stays the same, so it will not trigger a render! Nice!:
Mental Model
I still need some practice on my drawing skills, but in the images below we are visualizing the component render cycle.
An arrow represents a rerenderWhen we are using React.memo without useCallback, with every render the function will have a different reference. When React compares this prop with the previous prop, it will trigger a rerender because the function doesn't have the same reference.
See it rerenders every time, because the reference of the function is different on every render! When we implement useCallback the reference stays the same, so it will not trigger a rerender:
That's how useCallback works.
useMemo
One last note on the difference between useMemo and useCallback. If you need to maintain/memoize a reference between renders you should use useCallback(). If you need to memoize the value you should use useMemo(). Below you see what the hooks will memoize:
// memoized by useCallback()
console.log(func1); // () => "nice" + " car"
// memoized by useMemo()
console.log(func1()); // "nice car"
Remember to be careful with introducing performance optimizations in React. Only optimize when it's really needed, because optimization also comes with a cost!
Have fun!