I'm trying to learn about the hooks functionality, however I can't quite seem to figure out how I'm supposed to use the function useCallback
properly. As far as I understand from the rules about hooks I'm supposed to call them top-level and not within logic (such as loops). Which leads me to be quite confused how I'm supposed to implement useCallback
on components that are rendered from a loop.
Take a look at the following example snippet where I create 5 buttons with an onClick
handler that prints the number of the button to the console.
const Example = (props) => { const onClick = (key) => { console.log("You clicked: ", key); }; return( <div> { _.times(5, (key) => { return ( <button onClick={() => onClick(key)}> {key} </button> ); }) } </div> ); }; console.log("hello there"); ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id='root'> </div>
Right now it creates a new function instance everytime the Example renders due to my use of the arrow function within the <button onClick={() => onClick(key)}>
, which I do because I want the onClick
function to know which button called it. I thought I could prevent this with the new react hooks functionality by using useCallback
, however if I apply it to the const onClick
then it would still create a new instance every render because of the inline arrow function needed to give the key
parameter, and I'm not allowed to apply it to the render within a loop as far as I know (especially if the loop order might change right?).
So how would I go about implementing useCallback
in this scenario while keeping the same functionality? Is it at all possible?
The syntax As we can see, the useCallback React hook takes in an inline function and its dependencies as parameters and returns a memoized version of the function.
useMemo is very similar to useCallback. It accepts a function and a list of dependencies, but the difference between useMemo and useCallback is that useMemo returns the memo-ized value returned by the passed function. It only recalculates the value when one of the dependencies changes.
React's useMemo hook enables us to memoize the result of the execution of a function with a given set of parameters. Next time the function is called with the same parameter, we can return the data that has been cached, rather than re-executing the entire function.
UseCallback is used to optimize the rendering behavior of your React function components, while useMemo is used to memoize expensive functions to avoid having to call them on every render. As a standard construction of hooks, those two solutions are not so different.
The simple answer here is, you probably shouldn't use useCallback
here. The point of useCallback
is to pass the same function instance to optimized components (e.g. PureComponent
or React.memo
ized components) to avoid unnecessary rerenders.
You're not dealing with optimized components in this case (or most cases, I'd suspect) so there's not really a reason to memoize callbacks like with useCallback
.
Supposing the memoization is important, though, the best solution here is probably to use a single function instead of five: instead of a unique function for each button carries the key
by closure, you can attach the key
to the element:
<button data-key={key}>{key}</button>
And then read the key from the event.target.dataset["key"]
inside a single click handler:
const Example = (props) => { // Single callback, shared by all buttons const onClick = React.useCallback((e) => { // Check which button was clicked const key = e.target.dataset["key"] console.log("You clicked: ", key); }, [/* dependencies */]); return( <div> { _.times(5, (key) => { return ( <button data-key={key} onClick={onClick}> {key} </button> ); }) } </div> ); }; console.log("hello there"); ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script> <div id='root'> </div>
All that being said, it is possible to memoize multiple functions in a single hook. useCallback(fn, deps)
is equivalent to useMemo(() => fn, deps)
, and useMemo
can be used to memoize multiple functions at once:
const clickHandlers = useMemo(() => _.times(5, key => () => console.log("You clicked", key) ), [/* dependencies */]);
const Example = (props) => { const clickHandlers = React.useMemo(() => _.times(5, key => () => console.log("You clicked", key) ), [/* dependencies */]) return( <div> { _.times(5, (key) => { return ( <button onClick={clickHandlers[key]}> {key} </button> ); }) } </div> ); }; console.log("hello there"); ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script> <div id='root'> </div>
Perhaps there's a case where that's useful, but in this case I would either leave it alone (and not worry about the optimization) or else use a single handler for each button.
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