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

Add transform to support arrow function class properties #322

Merged
merged 14 commits into from
Sep 15, 2016

Conversation

calesce
Copy link
Collaborator

@calesce calesce commented Jul 13, 2016

I implemented this based off of the discussion in #242. I started with a failing test case where class properties didn't get hot-reloaded.

I still need to add the case for arrow functions without bodies, but I figured I'd go ahead and open up the discussion.

Also, I added a .babelrc to the AppContainer test directory to include the Babel plugin, so I could test the class property case. While the existing test cases are still passing, I'm not sure whether running the transform will change how they work, since they rely on registering/tagging components manually.

properly when the method is changed, as well as a failing test demonstrating that class properties do not
get replaced.
…ing tests. also restructure the babel plugin tests
@calesce
Copy link
Collaborator Author

calesce commented Jul 14, 2016

Ok, I fixed the case for arrow functions without block statement bodies. I also found a case where it failed, because the value field of a class property is nullable (which seems to happen when adding propType annotations to class components via Flow).

@gaearon
Copy link
Owner

gaearon commented Jul 14, 2016

Wow! Thanks for working on this.
I’ll definitely check it out soon.

@ro-savage
Copy link
Contributor

Awesome work @calesce !

We tried this out and found it broke when we had methods with default params. It seemed to work fine in all the other cases in our project.

Example:

class defaultParamsComponent extends React.Component {

  defaultParam = (param = 'test') => {
    return param
  }

  render() {
    return (
      <div>
        {this.defaultParam()}
      </div>
    )
  }
}

Gives the error

Property arguments[0] of CallExpression expected node to be of a type ["Expression","SpreadElement"] but instead got "AssignmentPattern"

@gaearon We'd love to see this implemented. Hot-reloading is pretty broken for us because we are using arrow function methods.

@calesce
Copy link
Collaborator Author

calesce commented Jul 27, 2016

Thanks for the feedback @ro-savage! I also just noticed it breaks when destructuring arguments, eg:
onClick = ({ target }) => {}. I should iron those two cases out tonight.

properties transform, since the parameters in the source code can
have default values, which don't need to be copied when calling
the new generated method
if the class properties function has destructured parameters,
instead of copying the destructuring expression, create new identifiers
for each param, and call the generated method with those new parameters
@ro-savage
Copy link
Contributor

@calesce I updated our react-hot-loader with your latest changes and its working perfectly for us. Thanks heaps!

@calesce
Copy link
Collaborator Author

calesce commented Jul 27, 2016

@ro-savage no problem!

I now realize that this type of transform won't work if a developer were to change the number of arguments when changing a class property function, since the argument count won't be reloaded. Which isn't good. I think something like this might work instead:

before:

class App {
  foo = (bar, baz) => {
    return bar + baz
  }
}

after:

class App {
  foo = (...params) => {
    return this.__foo__REACT_HOT_LOADER__(...params)
  }

  __foo__REACT_HOT_LOADER__(bar, baz) {
    return bar + baz
  }
}

That way, the number of arguments won't matter, so the developer can add/remove args and it should reload fine.

Edit:
Done, it works with the existing tests (although I had to change the Babel fixtures to match the new output), and anecdotally works well when playing around with it in my app.

This allows the class property functions to be hot-reloaded properly when
changing the number of arguments
@HriBB
Copy link

HriBB commented Aug 16, 2016

I tried your fork, and it works on a small app, but not on a larger application. I'm switching back to RHL1.3 for the moment. I hope you figure it out. Would love to help, but this is a bit over my head ATM.

@calesce
Copy link
Collaborator Author

calesce commented Aug 16, 2016

Hey thanks! Any details on where/how it doesn't work?

@HriBB
Copy link

HriBB commented Aug 17, 2016

@calesce will test it again tomorrow and report. Basically all I need is class arrow functions to hot-reload. I use them heavily throughout my app and without them being hot-reloaded is pretty much useless for me. For now I switched to binding them in constructor, and now they do reload, so I don't have to reload the app anymore :)

@ro-savage
Copy link
Contributor

@HriBB, can you find out what is not hot reloading?

I am using it on an app that solely uses arrow functions and contains >200 components and haven't ran into any problems.

@HriBB
Copy link

HriBB commented Aug 17, 2016

@calesce @ro-savage I must be doing something wrong, since even basic hot reload does not work. I'm not sure if I installed @calesce's fork properly. This is what I did.

git clone git@github.com:calesce/react-hot-loader.git
cd react-hot-loader
git checkout class-properties
npm install
cd ../my-project
npm link ../react-hot-loader
npm run client:dev

App loads fine, everything works except hot reload. I am using webpack-dev-server and resolve.alias.

When I change some file, I get the standard output in the console, but nothing happens.

[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.
[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR]  - 1511
[HMR]  - 1510
...
[HMR]  - 1568
[HMR]  - 1666
[HMR] App is up to date.

But nothing is changed. It works as expected with npm version of react-hot-loader@3.0.0-beta.2

Any ideas? I'm looking at the RHL source, but I'm not yet familiar with it and so I have no idea how to debug this ...

This is my config

@HriBB
Copy link

HriBB commented Aug 17, 2016

Ahh I finally got it to work. I guess that npm link is not the right way to install forks. Here's what I did. BTW I'm using node v6.3.1

git clone git@github.com:calesce/react-hot-loader.git
cd react-hot-loader
git checkout class-properties
npm install
rm -rf node_modules
npm install redbox-react react-proxy global react-deep-force-update
cd ..
cp -r react-hot-loader my-project/node_modules
cd my-project
npm run client:dev

@calesce @ro-savage how did you install the fork? I would really like to know so that I don't run into the same problems in the future.

It looks like it's working. I will do some more testing over the next few days and report any problems here ;)

@calesce
Copy link
Collaborator Author

calesce commented Aug 24, 2016

@HriBB: I think you needed to npm run build first.

The way I tested was copy-pasting the built files into my app's node_modules/react-hot-loader (where I already had beta3 installed).

npm link is probably the recommended/easier way, but I've only used it a few times so I'm not comfortable with it yet 😉

@HriBB
Copy link

HriBB commented Aug 24, 2016

@calesce yeah I know. It only worked if I manually copied files into my project's node_modules/react-hot-loader folder (after running npm run build in RHL3 fork).

If I used the npm link approach, I ended up with node_modules/react-hot-loader/node_modules/[all RHL3 dependencies] and hot reload would not work.

This is out of the scope of this PR, but does anyone know how to properly link forks?

@yasuf
Copy link

yasuf commented Sep 1, 2016

Any updates on this? I tried copying the built lib folder from the react-hot-loder repo on the class-properties branch into my project's node_modules but it didn't work, I get this error The following modules couldn't be hot updated: (They would need a full reload!), any ideas on what to try?

@calesce
Copy link
Collaborator Author

calesce commented Sep 2, 2016

@carlosyasu91 It's hard to say, any way you can push up a minimal project reproducing the issue?

}

wrapper.find('span').simulate('click');
expect(spy).toHaveBeenCalledWith('bar');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@calesce it might be worth it to test that the method is replaced without remounting the component, eg. like it is done here https://github.com/gaearon/react-hot-loader/blob/next/test/AppContainer/AppContainer.dev.js#L182

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, doesn't hurt.

@nfcampos
Copy link
Collaborator

nfcampos commented Sep 4, 2016

@calesce this looks awesome! what do you think of adding more isolated tests that do not include the es-2015 or stage-1 babel presets, to see what out plugin is actually doing on its own. something like this https://gist.github.com/nfcampos/275ada99b630ee9524b374b0588b36c3

@calesce
Copy link
Collaborator Author

calesce commented Sep 4, 2016

@nfcampos thanks for the review!

I agree on not needing run the ES2015/stage1 transforms on the babel fixtures, the expected output is more readable that way.

@nfcampos
Copy link
Collaborator

nfcampos commented Sep 4, 2016

@calesce one other thing i just remembered, we might have to leave alone class properties that use arguments in the body of the function, because the way babel compiles them (see http://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=stage-2&code=class%20Foo%20%7B%0A%20%20bar%20%3D%20(a%2C%20b)%20%3D%3E%20%7B%0A%20%20%20%20console.log(arguments)%0A%20%20%20%20return%20a(b)%3B%0A%20%20%7D%0A%7D ) they are the arguments of the constructor, and the way we compile them they are the arguments to the method we create on the class.

edit: same is true of the use of new.target (see http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation), not true of super because apparently it's not permissible to use it at all inside a class property (see http://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=stage-2&code=class%20Foo%20%7B%0A%20%20bar%20%3D%20(a%2C%20b)%20%3D%3E%20%7B%0A%20%20%20%20console.log(super)%0A%20%20%20%20return%20a(b)%3B%0A%20%20%7D%0A%7D)

Since the class properties proposal was moved to stage 2
at TC39, we no longer need the stage-1 dependency.
We only care about what the plugin does, and not about how Babel
transpiles ES2015/class properties transforms. This makes the fixtures
more immediately understandable.
verifying that the class property transform doesn't break the other
transform and cause components to remount.
}

}
;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related with this PR I think but what's up with this ; and the other one at the end?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed it was Babel weirdness, I was making the tests pass here. According to AST explorer (highlight the semicolon) that's an additional empty statement, I'll see if I'm accidentally creating one.

@nfcampos
Copy link
Collaborator

@calesce take a look at this, I think it addresses what I mentioned above of opting out on use of arguments or new.target calesce#1 sorry about the confusion of opening a PR against a PR, didn't know how else to do this, I think if you merge my PR this Pr will be updated with my changes

opt out of class properties transpile on use of arguments, new.target inside
@calesce
Copy link
Collaborator Author

calesce commented Sep 14, 2016

@nfcampos no problem, I think I would've have to add you to my fork's collaborators.

I'll take a look at why there are additional empty statements being created tonight, then this should be good to go 👍

@calesce
Copy link
Collaborator Author

calesce commented Sep 15, 2016

@nfcampos The semicolon insertion comes from #263, so not related.

Actually, it'd be nice if we could split out multiple plugins, so we can test them in isolation, but still be under a "root" react-hot-loader/babel plugin.

@janv
Copy link

janv commented Sep 23, 2016

I have encountered an issue with this when using static class properties

class Foo extends React.Component {
  static bar = () => 'baz'
}

Processed throught the react-hot transformation, bar will be removed from the class.

I have illustrated this in https://github.com/janv/react-hot-static-bug

@calesce
Copy link
Collaborator Author

calesce commented Sep 23, 2016

@janv: you're right, we're stripping the static keyword off of the properties. thanks for the repro!

@janv
Copy link

janv commented Oct 7, 2016

I ran into another issue with this.
I recently tried removing the es2015 preset from our babelrc in development, since we're doing all our development in Chrome whcih should not need that preset.

We have Components nested two levels deep, that are forwarding callbacks which we create as arrow function class properties.

Something like

class A {
  render(){
    return <B onClick={this.handleClick}/>
  }

  handleClick = () => this.setState(...)
}

class B {
  render(){
    return <button onClick={this.handleClick}/>
  }

  handleClick = () => this.props.onClick(...)
}

setState in A complains that the component is unmounted. This happens without any actual module replacement happening, i.e. right after the inital page load.

That should mean someone somewhere is keeping a reference to an original implementation of a function. I'll try to come up with a minimal executable example. Also, no sure why this only started happening after we removed babel-preset-es2015 yet

var _this = this;

class Foo {
bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this line.
Wouldn't a call to foo.bar() be forwarded to window.__bar__REACT_HOT_LOADER__ instead of the class instance? Since the first line binds _this to the global object and not the class instance?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@janv I'd noticed that before and thought it was weird too (like, how would it work at all if it was really outputting this code?), but that's apparently what happens when you use babel with only babel-plugin-syntax-class-properties, without the corresponding transform plugin. (which we're doing in these test fixtures). I'll fix these tests to use both plugins so that this confusing output doesn't confuse anyone in the future.

@janv
Copy link

janv commented Oct 7, 2016

I managed to write a minimal app that exhibits the problem:
https://github.com/janv/react-hot-static-bug/tree/arrow-bug

(Notice this is a new branch on the repo I used before to illustrate the issue with static class properties)

@calesce
Copy link
Collaborator Author

calesce commented Oct 7, 2016

Might be worth noting that we already were having problems when not using Babel to transpile classes (#313).

@calesce
Copy link
Collaborator Author

calesce commented Oct 9, 2016

@janv thanks again for the repro.

Yeah, that definitely seems like the same problem as #313. The issue remains even if you replace the class properties in your example with normal class methods.

I'm kind of surprised that "native" classes behave differently from how Babel transpiles them. The fix will probably have to come from react-proxy.

@nfcampos
Copy link
Collaborator

@calesce yep I believe the fix would have to be in react-proxy, I played around with it on friday but haven't figured out what exactly is different about native class that makes this fail

@calesce
Copy link
Collaborator Author

calesce commented Oct 10, 2016

Weirdly, it looks like react-proxy has test coverage for native classes.

@nfcampos
Copy link
Collaborator

@calesce yeah I noticed, not sure why the tests are not failing but this is

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants