-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Data: Provide dependencies from withSelect to useSelect #19007
Conversation
We tried this when originally developing the I don't think |
I'm not sure I understand this point. As implemented in this pull request (props passed as a member of the dependencies array), React isn't inspecting the object itself, only considering whether the reference value of the object differs from the previous reference value (i.e. I see in the discussion there that there was some back-and-forth about how to implement |
This is the React warning I got at the time (looks like it's still in source):
So the problem is if on render calc one you have something like this for const ownProps = { foo: 'bar', cheeseburgers: 'fries' }; And then on future render calc this was instead: const ownProps = { foo: 'bar' }; This is something that could typically happen if there is variation on the number of props passed through on subsequent render calcs in the tree. Of course all this is based on what I experienced at the time. |
Oh dang, sorry. I just realized, I was getting the error because at the time I was trying to expand the props into the dependency array and that's why I was getting the warning. Ignore everything I said. |
Yes, I could see this being a problem if this were implemented as: useSelect( mapSelect, ownProps ); Or some derivation from it: useSelect( mapSelect, Object.values( ownProps ) ); But if it's passed as a member of an array (as in this pull request): useSelect( mapSelect, [ ownProps ] ); ... then React shouldn't be bothering to inspect it aside from |
@@ -56,7 +56,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( | |||
ownProps, | |||
registry | |||
); | |||
const mergeProps = useSelect( mapSelect ); | |||
const mergeProps = useSelect( mapSelect, [ ownProps ] ); |
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.
ownProps is a shallow object in general, should we instead try to extract the values from that object as an array (and try to keep the order based on prop names)?
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.
ownProps is a shallow object in general, should we instead try to extract the values from that object as an array (and try to keep the order based on prop names)?
I don't think so, no. For the same reasons as discussed from earlier comments, your suggestion may be considered problematic for how React compares dependencies. But I also don't think it's necessary that we do this. pure
on the withSelect
should be enough to keep reference value updates limited to effective changes.
Even if we were to go down this route, I think it would greatly diminish any potential benefit we'd gain over simply running the selector on every render, since trying to derive the dependencies from ownProps
would itself be a fairly expensive operation.
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.
ownProps is a shallow object in general, should we instead try to extract the values from that object as an array (and try to keep the order based on prop names)?
This is exactly what I had tried in the original useSelect
pull which led to the React warning I mentioned (which as I noted later is not relevant to this particular pull as I originally thought).
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 don't think this pull should hurt anything as it's implementing useSelect
using a pattern already used elsewhere. A side-benefit is it models a pattern that should be used for any newer developers diving through our code.
I'll approve this, but there's still a number of failing e2e tests. On the surface it doesn't look like they should be related to this pull (I didn't look closely at any) but I'm assuming they'll be examined prior to merge.
This is breaking tests because In The only reasons for
For each of those reasons ✅ means it re-ran.
By not memoizing |
Where is this dependency established between Also a bit unsure if you're proposing that this change be abandoned, or if it's something where we can more-explicitly define this dependency between |
Here:
We need the behavior in I think we can avoid that without any repercussions, but making the dependency more explicit by providing the async mode as a dependency to |
Okay, I can do that. But I'm still not entirely clear on why we need to retain the previous behavior. We don't otherwise treat a refactor from |
Technically, in very-edge cases, it is.
None I can think of, but we would break third party tests. If the impact of that is not too big, we can add a dev note and move on I guess. |
I don't think this is clear to most people who are refactoring to use My preference would be to the latter but, as noted, I also don't feel confident in knowing what effective impact these changes have 😄 If it's agreeable, I can work to update the unit tests so they pass. |
Yeah, definitely not.
I also favor the latter approach to make things consistent. The "breaking change" isn't really breaking anything, but very specific implementation tests. At least, that's what it looks like now.
Let's do it. If anything big breaks, it will be easy to revert. |
82e174e
to
e1157df
Compare
expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); | ||
expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); | ||
expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); | ||
expect( OriginalComponent ).toHaveBeenCalledTimes( 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.
Verifying the changes from updating the tests, the other test updates make sense to me, but this one doesn't. I'm not sure why OriginalComponent
renders an extra time as a result of these changes. And it seems to happen any time a dependencies array is provided to useSelect
, regardless of whether it's a fixed value or not (i.e. still renders an extra time when providing []
). I'm not sure if this is a specific characteristic of useCallback
with dependencies (possibly related to facebook/react#14099?) and whether it really only has impact under a changing registry context that this test case is verifying against. It wouldn't be quite as worrying if it is only for the changing registry, since that doesn't happen quite so often in real-world usage.
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.
The first render is for the registry changing, and the second one is for the return value of mapSelectToProps
changing, right?
It seems to check out. We could also add a test that changes the state without changing the registry and 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.
The first render is for the registry changing, and the second one is for the return value of mapSelectToProps
changing, right?
It seems to check out. We could also add a test that changes the state without changing the registry and 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.
The first render is for the registry changing, and the second one is for the return value of
mapSelectToProps
changing, right?
Yes, I think that's what the original tests were trying to verify. But now there's a third unexplained render.
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 was only called once before.
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.
The mock count carries on from the previous render.
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'm referring to the change of:
Before:
expect( OriginalComponent ).toHaveBeenCalledTimes( 2 );
After:
expect( OriginalComponent ).toHaveBeenCalledTimes( 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.
Mmmm, have you tried seeing if ownProps
is somehow changed/invalidated? That could trigger an extra render.
Based on the failing end-to-end tests, there's definitely an impact of these changes. In this test, for example, the ArrowLeft second-to-last action will create a list item at the top-most list indentation, rather than the behavior on master where it moves the caret into the nested list item: gutenberg/packages/e2e-tests/specs/editor/blocks/list.test.js Lines 459 to 489 in 3687e8b
It's not really clear to me why this happens. It could still beg the question whether this is a change we want to make, but I suspect if the direction is to migrate to |
Sounds like timing issues with async mode and stale props. Maybe |
e1157df
to
f43b08d
Compare
Can you reproduce them outside of tests? Maybe it's just the tests not waiting long enough for something that before updated earlier due to unnecessary reruns? |
There's less value in these changes over time as we continue to migrate more components to use |
Previously: #15737
This pull request proposes to include a dependencies array in
withSelect
's rendering ofuseSelect
. The goal here is to reduce the number of calls tomapSelectToProps
, since otherwise the mapping callback would be called in every render. From my reading of #15737, I see that there is pretty extensive discussion of how dependencies should be managed, so perhaps I am overlooking some context here. I see thatpure
is used to avoid unnecessary renders of thewithSelect
higher-order component. SincemapSelectToProps
depends onownProps
, it must be defined as a dependency of theuseSelect
hook if dependencies are passed. Givenpure
should enforce thatownProps
reference changes only upon an effective shallow change in props, it should be enough to pass this object directly as the sole dependency touseSelect
. I think there may have been some expectation thatpure
would be enough to avoid excessive calls tomapSelect
, but since the component could render for reasons other than justwithSelect
,useSelect
's internal reference ofuseCallback
would always change when provided anundefined
deps
array, and thus always callmapSelect
even without a change in props or global state.With these changes, I observe a decrease of
mapSelect
calls on an initial rendering of the editor from 157 to 112. This is intended to serve as a simple reference point; it's expected this benefit should sustain over lifetime of the editor session.This follows some discussion at #18960 (comment) and for me is something of a personal study of if and how dependencies should apply to hooks, or if dependencies should always be provided when supported by a hook.
Performance Results:
In recent performance testing of other pull request, I've struggled to find any measurable difference (better or worse) using
npm run test-performance
. I'm not sure if this is an issue with my environment, the tests as written, or the fact that there's truly little benefit to these changes.For exhaustiveness' sake, I dove into the React source to seek some insight into whether the overhead of comparing dependencies in renders might incur a higher cost than any benefit to be gained by avoiding a call to
mapSelect
. This operation appears to be very trivial; not much more than a simple loop of the before/after deps [1] [2] [3].Testing Instructions:
It's assumed unit tests and end-to-end test cases should protect against regressions here.