TLDR: useHover may trigger unnecessary re-renders: demo, solution

This weekend I found The Guardian's blog post about their website migration to React. At the end of the post, they mention DCR. It's a frontend rendering framework for theguardian.com and it's available on Github. I was so interested in how it is designed inside so I've started my research.

Performance is one of things that I was interested in. So I've tried to find any usages of React.memo , PureComponent or shouldComponentUpdate . And I was so surprised when I found nothing.

I've gone back to their site and started profiling it. And I didn't found any unnecessary re-renders because it just renders article. The data never changes, the page doesn't have any tricky handlers. So any optimizations will be just extra costs here. But then I've found this.

It's a simple component to render most viewed articles aside. And it re-renders on a hover event because its content styled programmatically. So it looks logical: you hover X and it re-renders because now it's hovered. After you hover Y and X with Y re-render because the state of both components changed. But what if I show you this?

Blue and orange boxes is not a part of the component. React profiler shows it when a component re-renders

Mouse movement inside the component still trigger re-renders, but I had no idea why. I've wrapped internal components with React.memo , callbacks with useCallback and other things that usually help. But this component was still re-render. React profiler showed that props still change. Then I thought maybe useHover has some issues. But it didn't.

So I've written a plain html+css+js demo and shared with some friends to whine and complain why mouse movement inside a hovered element triggers mouseout and mouseover events. And they helped me. I have forgotten about a core mechanic of javascript events. The mechanic is event bubbling and capture. The demo extended with extra logging currentTarget shows it.

Unfortunately e.stopPropagation does not solve current issue, so I've implemented a throttling mechanism for that hook with setTimeout .

The original version of the demo is available here

As before, mouse event handlers trigger immediately (you may track it by console logs), but setState (prefixed with Deferred ) will be called on the next event loop tick only. So if we have two or more serial events, the hook won't call setState on every event, it will work only once with the latest true/false value.

It was a good reminder to don't forget javascript basics because React is just a library that builded on them.