For a while a would like to start using react function components with react hooks instead of class extending react component, but there is one thing which discourages me. Here is an example from very first intro of react hooks:
import React, { useState } from 'react'
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
function handleNameChange(e) {
setName(e.target.value);
}
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
There is a handleNameChange declaration used as a change handler for input. Let's imagine that Greeting component updates really really frequently because of some reason. Does change handle initialize every time on every render? From JavaScript aspect of view how bad is that?
Does change handle initialize every time on every render?
Yes. That's one reason for the useCallback hook.
From JavaScript aspect of view how bad is that?
Fundamentally, it's just creating a new object. The function object and the underlying function code are not the same thing. The underlying code of the function is only parsed once, usually into bytecode or a simple, quick version of compilation. If the function is used often enough, it'll get aggressively compiled.
So creating a new function object each time creates some memory churn, but in modern JavaScript programming we create and release objects all the time so JavaScript engines are highly optimized to handle it when we do.
But using useCallback avoids unnecessarily recreating it (well, sort of, keep reading), by only updating the one we use when its dependencies change. The dependencies you need to list (in the array that's the second argument to useCallback) are things that handleNameChange closes over that can change. In this case, handleNameChange doesn't close over anything that changes. The only thing it closes over is setName, which React guarantees won't change (see the "Note" on useState). It does use the value from the input, but it receives the input via the arguments, it doesn't close over it. So for handleNameChange you can leave the dependencies blank by passing an empty array as the second argument to useCallback. (At some stage, there may be something that detects those dependencies automatically; for now, you declare them.)
The keen-eyed will note that even with useCallback, you're still creating a new function every time (the one you pass in as the first argument to useCallback). But useCallback will return the previous version of it instead if the previous version's dependencies match the new version's dependencies (which they always will in the handleNameChange case, because there aren't any). That means that the function you pass in as the first argument is immediately available for garbage collection. JavaScript engines are particularly efficient at garbage collecting objects (including functions) that are created during a function call (the call to Greeting) but aren't referenced anywhere when that call returns, which is part of why useCallback makes sense. (Contrary to popular belief, objects can and are created on the stack when possible by modern engines.) Also, reusing the same function in the props on the input may allow React to more efficiently render the tree (by minimizing differences).
The useCallback version of that code is:
import React, { useState, useCallback } from 'react' // ***
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
const handleNameChange = useCallback(e => { // ***
setName(e.target.value) // ***
}, []) // *** empty dependencies array
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
Here's a similar example, but it also includes a second callback (incrementTicks) that does use something it closes over (ticks). Note when handleNameChange and incrementTicks actually change (which is flagged up by the code):
const { useState, useCallback } = React;
let lastNameChange = null;
let lastIncrementTicks = null;
function Greeting(props) {
const [name, setName] = useState(props.name || "");
const [ticks, setTicks] = useState(props.ticks || 0);
const handleNameChange = useCallback(e => {
setName(e.target.value)
}, []); // <=== No dependencies
if (lastNameChange !== handleNameChange) {
console.log(`handleNameChange ${lastNameChange === null ? "" : "re"}created`);
lastNameChange = handleNameChange;
}
const incrementTicks = useCallback(e => {
setTicks(ticks + 1);
}, [ticks]); // <=== Note the dependency on `ticks`
if (lastIncrementTicks !== incrementTicks) {
console.log(`incrementTicks ${lastIncrementTicks === null ? "" : "re"}created`);
lastIncrementTicks = incrementTicks;
}
return (
<div>
<div>
<label>
Name: <input value={name} onChange={handleNameChange} />
</label>
</div>
<div>
<label>
Ticks: {ticks} <button onClick={incrementTicks}>+</button>
</label>
</div>
</div>
)
}
ReactDOM.render(
<Greeting name="Mary Somerville" ticks={1} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
When you run that, you see that handleNameChange and incrementTicks were both created. Now, change the name. Notice that nothing is recreated (well, okay, the new ones aren't used and are immediately GC'able). Now click the [+] button next to ticks. Notice that incrementTicks is recreated (because the ticks it closes over was stale, so useCallback returned the new function we created), but handleNameChange is still the same.
Looking strictly from a JavaScript perspective (ignoring React), defining a function inside a loop (or inside another function that's called regularly) is unlikely to become a performance bottleneck.
Take a look at these jsperf cases. When I run this test, the function declaration case runs at 797,792,833 ops/second. It's not necessarily a best practice either, but it's often a pattern that falls victim to premature optimization from programmers who assume that defining a function must be slow.
Now, from React's perspective. It can become a challenge for performance is when you are passing that function to child components who end up re-rendering because it's technically a new function each time. In that case it becomes sensible to reach for useCallback to preserve the identity of the function across multiple renders.
It's also worth mentioning that even with the useCallback hook, the function expression is still redeclared with each render, it's just that it's value is ignored unless the dependency array changes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With