React Hooks: memoize click handlers for list?

Viewed 723

I am trying to render a list of items, each of which has a button which will toggle a dropdown menu when pressed.

My code looks something like:

function ItemList({items}) {
  const [isDropdownExpanded, setIsDropdownExpanded] = useState(null);
  const itemClickHandlers = useMemo(() => {
    const handlers = {}
    items.forEach(item => {
      handlers[item.id] = value => setIsDropdownExpanded(value ? item.id : null);
    });
    return handlers;
  }, [items.map(item => item.id).join(',')]);
}
return (
  <ul>
  {items.map(item =>
    <li onClick={itemClickHandlers[item.id]}>
      <Item name={item.name} isDropdownExpanded={isDropdownExpanded === item.id}/>
    </li>
  )}
  </ul>
);

The above code works, but wastes renders when the user adds or deletes an item from the list. (itemClickHandlers generates a new object with all new handlers, even though only one handler should be created or removed, so every item in the list re-renders because isDropdownExpanded will not be equal by reference equality.)

Preferably using functional React components only, how can I render a list of items like this with minimal re-renders when the list of items changes?

1 Answers

You don't need handler for each item in array. All you need is to have one handler to which you pass on the id which can be obtained from event if you add the id on the li element

function ItemList({items}) {
  const [isDropdownExpanded, setIsDropdownExpanded] = useState(null);
  const itemClickHandlers = useCallback((e) => {
      const id = e.currentTarget.id;
      setIsDropdownExpanded(prev => (prev != id ? id: null));
  }, [])

    return (
      <ul>
      {items.map(item =>
        <li key={item.id} id={item.id} onClick={itemClickHandler}>
          <Item name={item.name} isDropdownExpanded={isDropdownExpanded === item.id}/>
        </li>
      )}
      </ul>
    );
}
Related