-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Conversation
Make sure to disable the "whitespace" changes when looking at the diff :) |
1d91dc8
to
b3052b7
Compare
453dc59
to
9fd8640
Compare
b3052b7
to
28c5c95
Compare
This is not blocked anymore and should be ready for a final review. |
e30379f
to
90e4af8
Compare
Would love to get a review here :) |
Please review :P @WordPress/gutenberg-core |
hoverArea = isRTL ? 'left' : 'right'; | ||
} | ||
let newHoveredArea = null; | ||
if ( ( event.clientX - left ) < width / 3 ) { |
There was a problem hiding this comment.
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.
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)) |
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. |
Ah, this is what I was referring to. I didn't immediately grasp what was being changed. |
Should we merge this? |
Based on the PR's scope this looks good. |
90e4af8
to
4b513dd
Compare
There was a problem hiding this 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 ); |
There was a problem hiding this comment.
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 asnull
usinguseRef
, we're safe to know that it's a DOM reference, presumably by the fact thatuseEffect
callback will occur after the initial render? - Does
wrapper
need to be a dependency of theuseEffect
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?)
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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