-
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: Fix issue with in-stack unsubscribe #5266
Conversation
data/index.js
Outdated
// behavior causes one further in the stack to be unsubscribed. The | ||
// latter's callback should not be called, which requires monitoring | ||
// changes to the array as they occur in iteration. | ||
for ( let i = 0; i < listeners.length; i++ ) { |
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.
If a listener removes itself, I think the keys of the listener could change in a way, we'd miss one listener call right?
Also, I believe the listeners.length
is performed once and thus listeners[ i ]
could be undefined at some point? And if a listener is added in another listener, should we call these listeners :)
Ok my brain crashed
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.
If a listener removes itself, I think the keys of the listener could change in a way, we'd miss one listener call right?
May be! I'll add a test and check.
Also, I believe the
listeners.length
is performed once and thuslisteners[ i ]
could be undefined at some point?
I'm not sure this is true, but I'll have to check the specification. I know that this used to be an optimization that would be commonly recommended:
for ( var i = 0, len = listeners.length; i < len; i++ ) {}
...but that browsers have since largely internally optimized. The fact that it was ever suggested should imply that it's checked on each iteration (and that, if browsers optimize, they do so safely to still allow for the original specification behavior).
And if a listener is added in another listener, should we call these listeners :)
Considering at a high level? I'm not really sure. In practice, I expect it probably should be called in the current implementation, since it pushes a new listener into the stack. I guess prior to these changes it would not have happened.
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.
FWIW, I recalled this being internally handled in Redux as well:
I might review their implementation to see if we can mimic the same expectations.
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.
Actually, on a cursory glance, that implementation would be represented with the current implementation prior to these proposed changes. But obviously we still have the issue of setState
being called after unsubscribe. At worst, we could add some logic to see that the withSelect
component is still mounted before calling this.setState
.
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.
What if we keep the previous implementation and just check that the listener is still in the array before calling it?
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 like the consistency in expected behavior of Redux:
If you subscribe or unsubscribe while the listeners are being invoked, this
will not have any effect on thedispatch()
that is currently in progress.
I'd say that we don't need to adopt the same behavior, but we should aim for similar consistency. If removing a listener takes effect immediately, then so should adding a listener, and vice-versa.
The more I think about it, the more I think we should just adopt this same behavior and include a check from within withSelect
to achieve the desired behavior.
i.e.
diff --git a/data/index.js b/data/index.js
index abd3f9429..ea38314e9 100644
--- a/data/index.js
+++ b/data/index.js
@@ -169,6 +169,8 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => {
componentWillUnmount() {
this.unsubscribe();
+
+ this.isUnmounting = true;
}
subscribe() {
@@ -176,6 +178,10 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => {
}
runSelection( props = this.props ) {
+ if ( this.isUnmounting ) {
+ return;
+ }
+
const newState = mapStateToProps( select, props );
if ( ! isEqualShallow( newState, this.state ) ) {
this.setState( newState );
Thoughts?
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.
Sounds good to me 👍
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.
LGTM 👍
This pull request seeks to resolve an issue with the
data
module's unsubscribe behavior which can result in a listener callback being invoked even after its been unsubscribed. In practice, this has been observed in #5206 where awithSelect
'ssetState
would be called even after the component has unmounted. The solution is using afor
loop in place ofArray#forEach
, where the latter does not account for modifications which occur to the underlying array while it is being iterated over. Since we might assume a listener's behavior could cause a later listener to become unsubscribed, we should support this by operating on the raw array, including modifications, while iterating.Testing instructions:
Ensure unit tests pass: