I have a working class based implementation of an Accordion component which I'm trying to refactor to use the new hooks api.
My main challenge is to find a way to re-render only the toggled <AccordionSection /> while preventing all the other <AccordionSection/> components from re-rendering every time the state of the parent <Accordion/> (which keeps track of the open sections on its state) is updated.
On the class-based implementation I've managed to achieve this by making the <AccordionSection /> a PureComponent, passing the isOpen and onClick callbacks to it via a higher-order component which utilizes the context API, and by saving these callbacks on the parent <Accordion/>'s component's state as follows:
this.state = {
      /.../
      onClick: this.onClick,
      isOpen: this.isOpen
    };
which, to my understanding, keeps the reference to them and thus prevents them from being created as new instances on each <Accordion /> update.
However, I can't seem to get this to work with the hooks-based implementation.
Some of the things I've already tried to no success:
Wrapping the Accordion section with memo - including various render conditions on the second callback argument.
wrapping the onClick and isOpen callbacks with useCallback (doesn't seem to work since they have dependencies which update on each <Accordion/> render)
saving the onClick and isOpen to the state like this: const [callbacks] = useState({onClick, isOpen}) and then passing the callbacks object as the ContextProvider value. (seems wrong, and didn't work) 
Here are the references to my working class-based implementation:
https://codesandbox.io/s/4pyqoxoz9
and my hooks refactor attempt:
https://codesandbox.io/s/lxp8xz80z7
I kept the logs on the <AccordionSection/> render in order to demonstrate which re-renders I'm trying to prevent.
Any inputs will be very appreciated.
so I ended up adding this little nugget after chasing too many rabbits..
const cache = {};
const AccordionSection = memo(({ children, sectionSlug, onClick, isOpen }) => {
  if (cache[sectionSlug]) {
    console.log({
      children: children === cache[sectionSlug].children,
      sectionSlug: sectionSlug === cache[sectionSlug].sectionSlug,
      onClick: onClick === cache[sectionSlug].onClick,
      isOpen: isOpen === cache[sectionSlug].isOpen
    });
  }
  cache[sectionSlug] = { children, sectionSlug, onClick, isOpen };
This showed that it was onClick that was changing. Which then seems obvious as the Accordion component is rendering and creating a new onClick.
wrapping he onClick creation with useCallback rectifies the issue.
const onClick = useCallback(
  sectionSlug =>
    setOpenSections({
      ...(exclusive ? {} : openSections),
      [sectionSlug]: !openSections[sectionSlug]
    }),
  []
);
though I do seem to have broken exclusive in the process as it's always enabled now..
https://codesandbox.io/s/1o08p08m27
oh, I did move a few other pieces around in there that might have contributed to the fix..
Update
refactored to use useReducer and moved all the logic there so we can deliver a stable onClick
Update
they say sleep is good, but for me it's just trying to get to sleep..
I knew there was something I was missing.. realised last night we don't need the reducer, just the function form of setState which allows us to access the up-to-date state from within the useCallback memoed function. Converted @itaydafna's optimisation here https://codesandbox.io/s/8490v55029
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