React hooks useCallback with parameters inside loop
Asked Answered
R

2

32

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?

Rattrap answered 5/3, 2019 at 15:18 Comment(0)
P
30

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.memoized 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.

Patricide answered 6/3, 2019 at 4:49 Comment(3)
The data-* attributes part of this answer is also provided in the React documentation for optimizing callback events for a large number of elementsCoronel
OP didn't ask for opinions on when it's appropriate to useCallback. This is a non-answer.Mccalla
@tshm001 They asked about "proper usage" and sometimes the proper usage is to not use it. I did show how to memoize an array of callbacks (useMemo is the same purpose as useCallback). If you've got a better answer, feel free to post it.Patricide
R
0

You need to use a closure, and you can do so even with useCallback;

With your example:


const Example = (props) => {
  const handleClick = useCallback((key) => (event) => {
    console.log("You clicked: ", key);
  }, []);
  
  return(
    <div>
      {
        _.times(5, (key) => {
          return (
            <button onClick={handleClick(key)}>
              {key}
            </button>
          );
        })
      }
    </div>
  );
};

This example is simple without any need to refresh the handleClick, but be careful with your memoized functions though, they usually need to be updated if you use props within the closure. Update the [] accordingly.

Riparian answered 24/7, 2023 at 22:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.