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

[ScrollView] Remove mixins from ScrollView #22374

Closed
wants to merge 4 commits into from

Conversation

thymikee
Copy link
Contributor

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 using mixins API, in favor of populating _scrollResponder field with ScrollResponder.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

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 21, 2018
@pull-bot
Copy link

Warnings
⚠️

📋 Release Notes - This PR appears to be missing Release Notes.

Generated by 🚫 dangerJS

);
}
}
this._scrollResponder.props = this.props;
Copy link
Member

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@RSNara RSNara left a 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;
Copy link
Contributor

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: {},
Copy link
Contributor

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,
Copy link
Contributor

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;
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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)

Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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);
Copy link
Contributor Author

@thymikee thymikee Nov 22, 2018

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

Copy link
Contributor

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.

Copy link
Contributor

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?

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 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)

Copy link
Contributor

@RSNara RSNara 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 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);
Copy link
Contributor

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);
Copy link
Contributor

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();
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor

@facebook-github-bot facebook-github-bot left a 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.

@react-native-bot
Copy link
Collaborator

@thymikee merged commit 221e2fe into facebook:master.

@facebook facebook locked as resolved and limited conversation to collaborators Dec 26, 2018
@react-native-bot react-native-bot added the Merged This PR has been merged. label Dec 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Component: ScrollView Component: View Merged This PR has been merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants