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