-
Notifications
You must be signed in to change notification settings - Fork 24.4k
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
[ScrollView] Remove mixins from ScrollView #22374
Conversation
Generated by 🚫 dangerJS |
); | ||
} | ||
} | ||
this._scrollResponder.props = this.props; |
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 seems like it would be problematic if the props passed to ScrollView change
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.
Yeah, this is pretty brittle.
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.
One way you could get around this is to define a getter.
Object.defineProperty(this._scrollResponder, 'props', {
get: () => {
return this.props;
}
})
But I'm not sure if that's a good idea because it'll be difficult for flow to type check this.
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.
Thank you for this PR! 😁
I think most of this is good, but we should go one step further and turn this into a React.Component subclass. 🤔
); | ||
} | ||
} | ||
this._scrollResponder.props = this.props; |
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.
Yeah, this is pretty brittle.
@@ -551,22 +551,33 @@ export type Props = $ReadOnly<{| | |||
*/ | |||
const ScrollView = createReactClass({ | |||
displayName: 'ScrollView', | |||
mixins: [ScrollResponder.Mixin], | |||
|
|||
_scrollResponder: {}, |
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 a good idea to just turn this into a React.Component subclass right now:
const ScrollResponder = require('ScrollResponder');
function createScrollResponder(node: React.ElementRef<typeof ScrollView>): typeof ScrollResponder.Mixin {
const scrollResponder = {... ScrollResponder.Mixin};
for (const key of Object.keys(ScrollResponder.Mixin)) {
if (typeof scrollResponder[key] === 'function') {
scrollResponder[key] = scrollResponder[key].bind(node);
}
}
return scrollResponder;
}
class ScrollView extends React.Component<Props, State> {
_scrollResponder: typeof ScrollResponder.Mixin = createScrollResponder(this);
}
Then, we change all references to scrollResponder methods within ScrollView
to use this._scrollResponder
for (const key in ScrollResponder.Mixin) { | ||
if (typeof ScrollResponder.Mixin[key] === 'function') { | ||
this._scrollResponder[key] = ScrollResponder.Mixin[key].bind( | ||
this._scrollResponder, |
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 avoid having to proxy props
and state
, you should just bind this to the component instance.
); | ||
} | ||
} | ||
this._scrollResponder.props = this.props; |
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.
One way you could get around this is to define a getter.
Object.defineProperty(this._scrollResponder, 'props', {
get: () => {
return this.props;
}
})
But I'm not sure if that's a good idea because it'll be difficult for flow to type check this.
getInitialState: function() { | ||
this._createScrollResponder(); |
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 not too sure it's a good idea to hack getInitialState
to initialize the scrollResponder. What we really need is a constructor, and we can get that by converting this component to a class, which shouldn't take too much time. I think we should just do that instead. Besides, we're going to have to remove this code eventually when we convert to a class down the line anyways.
@@ -676,7 +688,7 @@ const ScrollView = createReactClass({ | |||
* @platform ios | |||
*/ | |||
flashScrollIndicators: function() { | |||
this.getScrollResponder().scrollResponderFlashScrollIndicators(); | |||
this._scrollResponder.scrollResponderFlashScrollIndicators(); |
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 think we should just change getScrollResponder()
to return this._scrollResponder
.
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.
Not sure about that. According to the comments getScrollResponder()
is used to e.g. fire scrollTo()
which is not available in ScrollResponder.Mixin
. Unless they're wrong (there's no actual usage of getScrollResponder()
in this repo, so it may relate to internal FB callsites)
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.
Good call. 😅
layoutHeight: null, | ||
}; | ||
}, | ||
|
||
UNSAFE_componentWillMount: function() { | ||
this._scrollResponder.UNSAFE_componentWillMount(); |
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.getScrollResponder().UNSAFE_componentWillMount();
@@ -586,6 +597,7 @@ const ScrollView = createReactClass({ | |||
}, | |||
|
|||
componentWillUnmount: function() { | |||
this._scrollResponder.componentWillUnmount(); |
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.getScrollResponder(). componentWillUnmount();
for (const key in ScrollResponder.Mixin) { | ||
if (typeof scrollResponder[key] === 'function') { | ||
scrollResponder[key] = scrollResponder[key].bind(this); | ||
(this: any)[key] = scrollResponder[key].bind(this); |
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.
Turning this to class component turned out pretty straightforward. However, problem with binding scrollResponder
to ScrollView
is that methods from mixin will refer to ScrollView
context, while they were not assigned to the ScrollView instance, but to this._scrollResponder
. To mitigate that, I added an ugly temporary hack that copies methods from mixin back to the ScrollView
. Wonder if it makes sense to actually do only that and only put lifecycle methods (componentWillMount/componentWillUnmount) inside this._scrollResponder
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.
Right. I completely overlooked the fact that mixin methods use this
to access sibling methods. 😅
What I was worried about was mixin methods using component methods, or other methods defined in the mixins. Like, ScrollResponder.Mixin.scrollResponderGetScrollableNode
, which calls this.getScrollableNode
:
/**
* Returns the node that represents native view that can be scrolled.
* Components can pass what node to use by defining a `getScrollableNode`
* function otherwise `this` is used.
*/
scrollResponderGetScrollableNode: function(): ?number {
return this.getScrollableNode
? this.getScrollableNode()
: ReactNative.findNodeHandle(this);
},
this.getScrollableNode
is implemented on ScrollView
, if you look further down in the file:
getScrollableNode: function(): any {
return ReactNative.findNodeHandle(this._scrollViewRef);
},
But I think you took care of this with your 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.
🤔
Wouldn't this make the component lifecycle methods in ScrollResponder.Mixin
override the component lifecycle methods of the ScrollView? In other words, when react executes UNSAFE_componentWillMount
, wouldn't it actually execute the UNSAFE_componentWillMount
of ScrollResponder.Mixin
?
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 wanted to address that, but I left it in a, let's say, "request for comments" state and lost track on this issue – this was the reason I left the this._scrollResponder
so we can differentiate on state and lifecycle of mixin vs component. This is what I meant while writing:
Wonder if it makes sense to actually do only that and only put lifecycle methods (componentWillMount/componentWillUnmount) inside this._scrollResponder
So I think it's gonna be safer to revert this change you landed locally and get this sorted here (unless you'd like to to fix it on your own while landing it, then be my guest :D)
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 good!
Thanks for addressing the changes so quickly! (And sorry, for getting back so late 😔). I'll pull the changes internally, test this out, (fix it up if need be), and land it within the next few days.
for (const key in ScrollResponder.Mixin) { | ||
if (typeof scrollResponder[key] === 'function') { | ||
scrollResponder[key] = scrollResponder[key].bind(this); | ||
(this: any)[key] = scrollResponder[key].bind(this); |
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.
Right. I completely overlooked the fact that mixin methods use this
to access sibling methods. 😅
What I was worried about was mixin methods using component methods, or other methods defined in the mixins. Like, ScrollResponder.Mixin.scrollResponderGetScrollableNode
, which calls this.getScrollableNode
:
/**
* Returns the node that represents native view that can be scrolled.
* Components can pass what node to use by defining a `getScrollableNode`
* function otherwise `this` is used.
*/
scrollResponderGetScrollableNode: function(): ?number {
return this.getScrollableNode
? this.getScrollableNode()
: ReactNative.findNodeHandle(this);
},
this.getScrollableNode
is implemented on ScrollView
, if you look further down in the file:
getScrollableNode: function(): any {
return ReactNative.findNodeHandle(this._scrollViewRef);
},
But I think you took care of this with your update.
for (const key in ScrollResponder.Mixin) { | ||
if (typeof scrollResponder[key] === 'function') { | ||
scrollResponder[key] = scrollResponder[key].bind(this); | ||
(this: any)[key] = scrollResponder[key].bind(this); |
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.
🤔
Wouldn't this make the component lifecycle methods in ScrollResponder.Mixin
override the component lifecycle methods of the ScrollView? In other words, when react executes UNSAFE_componentWillMount
, wouldn't it actually execute the UNSAFE_componentWillMount
of ScrollResponder.Mixin
?
@@ -676,7 +688,7 @@ const ScrollView = createReactClass({ | |||
* @platform ios | |||
*/ | |||
flashScrollIndicators: function() { | |||
this.getScrollResponder().scrollResponderFlashScrollIndicators(); | |||
this._scrollResponder.scrollResponderFlashScrollIndicators(); |
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.
Good call. 😅
const scrollResponder = {...ScrollResponder.Mixin}; | ||
for (const key in ScrollResponder.Mixin) { | ||
if (typeof scrollResponder[key] === 'function') { | ||
scrollResponder[key] = scrollResponder[key].bind(this); |
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.
Since you are copying all the methods from ScrollResponder to the component instance, why do we need to also create the scrollResponder here? Why not just call the scrollResponder methods directly off of the component instance? Is it because of flow?
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.
@RSNara has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
As a part of #22301 it turned out that we need to first convert
ScrollView
to class component. As a first step to do so, here's removal of usingmixins
API, in favor of populating_scrollResponder
field withScrollResponder.Mixin
(still used) methods.Test Plan:
Played around with RNTester app and looks ok, but I'd like more eyes on it.
Changelog:
[ScrollView][Removed] – Remove mixins from ScrollView
cc @TheSavior