Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the hovered area component to a hook #15038

Merged
merged 1 commit into from
Jun 6, 2019

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Apr 18, 2019

The idea here is that by using a hook, we don't need to pass the "wrapper" as a prop which means we don't need to "force rerender". I'm not sure this has a significant impact on performance but at least it avoids one extra re-rendering and the code is way cleaner.

Testing instructions

  • Make sure the block movers only show up when you hover the left area of the block (and the right in RTL languages)

@youknowriad youknowriad added [Type] Performance Related to performance efforts [Type] Code Quality Issues or PRs that relate to code quality labels Apr 18, 2019
@youknowriad
Copy link
Contributor Author

Make sure to disable the "whitespace" changes when looking at the diff :)

@youknowriad youknowriad force-pushed the update/block-component-hooks branch from 453dc59 to 9fd8640 Compare May 7, 2019 09:34
@youknowriad youknowriad changed the base branch from update/block-component-hooks to master May 7, 2019 11:22
@youknowriad youknowriad requested a review from nerrad May 7, 2019 11:36
@youknowriad
Copy link
Contributor Author

This is not blocked anymore and should be ready for a final review.

@youknowriad youknowriad force-pushed the add/use-hovered-area-hook branch 3 times, most recently from e30379f to 90e4af8 Compare May 7, 2019 14:40
@youknowriad
Copy link
Contributor Author

Would love to get a review here :)

@youknowriad
Copy link
Contributor Author

Please review :P @WordPress/gutenberg-core

hoverArea = isRTL ? 'left' : 'right';
}
let newHoveredArea = null;
if ( ( event.clientX - left ) < width / 3 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have width / 3 extracted into a variable? this way it would be easier to reason about this at first sight.

@aduth
Copy link
Member

aduth commented May 21, 2019

I have similar sentiments as to #15053 (review) when approaching this.

Would you want to apply a similar approach from there as in extracting hooks? (#15053 (comment))

@youknowriad
Copy link
Contributor Author

I have similar sentiments as to #15053 (review) when approaching this.

If you're speaking about the BlockListBlock component, you're right but this PR don't touch that component much (check the diff without the "whitespaces" changes).

If you're talking about the useHoveredArea itself, IMO it's simple enough and doesn't really need splitting.

@aduth
Copy link
Member

aduth commented May 21, 2019

If you're speaking about the BlockListBlock component, you're right but this PR don't touch that component much (check the diff without the "whitespaces" changes).

Ah, this is what I was referring to. I didn't immediately grasp what was being changed.

@youknowriad
Copy link
Contributor Author

Should we merge this?

@nicolad
Copy link
Member

nicolad commented May 31, 2019

Based on the PR's scope this looks good.
I've started another refactor related to hooks adoption: #15934

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a very nice refactor of the introduction of the hook-based useHoverArea, the flattening of BlockListBlock, and the avoidance of a forced re-render. 👍

render() {
const { hoverArea } = this.state;
const { children } = this.props;
wrapper.current.addEventListener( 'mousemove', onMouseMove );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, a few questions for my own clarification:

  • Even though the wrapper passed from block.js is initialized as null using useRef, we're safe to know that it's a DOM reference, presumably by the fact that useEffect callback will occur after the initial render?
  • Does wrapper need to be a dependency of the useEffect or are we okay to assume that it will never change (I assume this will help us to avoid needing to unbind / rebind event handlers too?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though the wrapper passed from block.js is initialized as null using useRef, we're safe to know that it's a DOM reference, presumably by the fact that useEffect callback will occur after the initial render?

yes, also took a look at some react community hooks and a lot of them use a similar technique.

Does wrapper need to be a dependency of the useEffect or are we okay to assume that it will never change (I assume this will help us to avoid needing to unbind / rebind event handlers too?)

This is the kind of questions I'm still unsure about. My naive thinking would say that leaving it out is better in terms of performance especially because we know that it doesn't change. (Same here, community hooks based on refs don't add it, at least the ones I saw).

@@ -105,13 +106,14 @@ function BlockListBlock( {
const wrapper = useRef( null );
useEffect( () => {
blockRef( wrapper.current, clientId );
// We need to rerender to trigger a rerendering of HoverArea.
rerender();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is where we get most of the performance benefit? We're currently rendering twice for every component update?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think in my tests, the difference is not really noticeable but yeah it's way cleaner that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify we're not rerendering on every update but we're rendering twice on mount.

Copy link
Member

@nicolad nicolad Jun 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to reason about componentDidMount in a functional component with something like this:
https://github.com/streamich/react-use/blob/master/docs/useMount.md
?
shall I add this utility function ? not the whole library

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read in several places that ideally when using hooks we should try to avoid thinking in terms of lifecycle hooks (did mount, did update) and instead thinking in terms of effects as results of props changes, state changes... Makes me wonder if we want to integrate this kind of hooks.


if ( hoverArea !== this.state.hoverArea ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're losing this condition in the refactor, which means we're setting state on every mousemove event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so as well, but we're not in my tests. I think this is automatically taken care of by react in its useState hook.

Also, adding this test is not a good idea, at least not without moving from useState into useReducer. When using useState it's not safe to use the "previous" state when setting a new one as the previous one might not be the exact previous state since concurrent setState calls can happen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so as well, but we're not in my tests. I think this is automatically taken care of by react in its useState hook.

Huh! That's very nice.

For my own curiosity, I found the documented reference to this behavior:

Bailing out of a state update

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update

@youknowriad youknowriad merged commit 2564c21 into master Jun 6, 2019
@youknowriad youknowriad deleted the add/use-hovered-area-hook branch June 6, 2019 17:27
@youknowriad youknowriad added this to the Gutenberg 5.9 milestone Jun 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Type] Code Quality Issues or PRs that relate to code quality [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants