17

When passing callback to component, I should use useCallback hook to return a memoized callback (to prevent unneeded renders):

import doSomething from "./doSomething";
const FrequentlyRerenders = ({ id }) => {
  const onEvent = useCallback(() => doSomething(id), [id]);
  return (
    <ExpensiveComponent onEvent={ onEvent } />
  );
};

But what if I am using map? for example:

import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
  return ids.map(id => {
    const onEvent = useCallback(() => doSomething(id), [id]);
    return (
      <ExpensiveComponent key={id} onEvent={ onEvent } />
    );
  });
};

How should I properly use useCallback? Is the above the right way to pass multiple callbacks? Is it really works and know to memioze every callback according to an item of an array?

Naor
  • 23,465
  • 48
  • 152
  • 268

2 Answers2

19

Convert the returned mapped JSX into a component and then you can useCallback without problems

import doSomething from "./doSomething";
const MappedComponent =(props) => {
   const onEvent = useCallback(() => doSomething(props.id), []);
   return (
      <ExpensiveComponent onEvent={ onEvent } />
   );
}

const FrequentlyRerendersMap = ({ ids }) => {
  return ids.map(id => {
    return <MappedComponent key={id} id={id} />
  });
};
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 2
    I thought about it, but I can't due to system architecture. Is there a way to fix it without creating a new component? – Naor May 03 '19 at 06:57
  • How is it a system architecture change. You create a component to optimise on performance.The other way to handle it would involve you making a change in ExpensiveComponent whereby you pass on the id to it as a prop along with doSomething and call it from within ExpensiveComponent – Shubham Khatri May 03 '19 at 07:00
  • For that matter, you could use [`memo`](https://reactjs.org/docs/react-api.html#reactmemo) on `MappedComponent` and avoid `useCallback()` altogether. – dx_over_dt Nov 09 '21 at 18:06
  • yes, but this does not work for the children prop, (for items like , that is a common child to a Google map. How can one memo() component children? Without this, the map flickers (due to rerendering). – cameron cameron Mar 23 '22 at 07:47
-2

This is now expressly discouraged in the docs. https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

Old answer

The answer to re architect is sidestepping the question IMO. Though, I think creating a new component is probably a good idea.

To answer the question though, your code:

import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
  return ids.map(id => {
    const onEvent = useCallback(() => doSomething(id), [id]);
    return (
      <ExpensiveComponent key={id} onEvent={ onEvent } />
    );
  });
};

Is actually what you would want to do to memoize in a map. I don't know the implementation of useCallback, but it should add very little memory overhead. A stackFrame, and whatever they do to reduce the array into some kind of key for the memoized function.

Unless your working on some with a TON of elements, EG react virtualized components unlimited scrolling, you are realistically safe to useCallback the way you are. In fact, the small memory overhead is probably a much cheaper price than the rerender of all of those components.

Eric Wooley
  • 646
  • 4
  • 17
  • I am not sure what will be the behaviour in case of a list change. – Naor Jul 25 '19 at 06:27
  • If you pass a different id in the array of dependencies, it should return a different function. – Eric Wooley Jul 26 '19 at 17:33
  • This gives me an error: `React Hook "useCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks` – Tom Jan 17 '20 at 22:02
  • @Tom Might be that it changed with the react version? Or you have this logic nested inside a useCallback? – Eric Wooley Jan 17 '20 at 23:11
  • This pattern is not expressly discouraged in the docs though https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level – Eric Wooley Jan 17 '20 at 23:18
  • @EricWooley the documentation you linked explicitly says "Don’t call Hooks inside loops, conditions, or nested functions.", and the .map's callback is a nested function. – Tom Jan 18 '20 at 00:52
  • @Tom I dupred, I meant to say "*now* expressly..." Sorry! – Eric Wooley Jan 19 '20 at 01:02