Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refactoring a React PureComponent to a hooks based functional component

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:

  1. Wrapping the Accordion section with memo - including various render conditions on the second callback argument.

  2. wrapping the onClick and isOpen callbacks with useCallback (doesn't seem to work since they have dependencies which update on each <Accordion/> render)

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

like image 967
itaydafna Avatar asked Mar 18 '19 10:03

itaydafna


1 Answers

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

like image 139
lecstor Avatar answered Sep 21 '22 13:09

lecstor