Skip to content
This repository has been archived by the owner on Sep 29, 2020. It is now read-only.

@autobind doesn't work correctly with react-hot-loader <= 1.3.0 #48

Open
wmertens opened this issue Feb 20, 2016 · 31 comments
Open

@autobind doesn't work correctly with react-hot-loader <= 1.3.0 #48

wmertens opened this issue Feb 20, 2016 · 31 comments

Comments

@wmertens
Copy link

I am using @autoBind together with React and Redux. It works in several classes, but I have one class that it doesn't work with. I tried eliminating a bunch of things like @connect but I can't make it work.

Here's an example of how I use it, but this does work:

import React from 'react'
import {autobind} from 'core-decorators'

export default class TestMe extends React.Component {
        test() {
           alert('whee it works')
        }
    @autobind
    doSomething() {
        this.test()
    }render() {
        return (
            <button onClick={this.doSomething}>clickme</button>
        )
    }
}

Can you think of anything that would make the autobind fail? Hot reloading, other decorators, bugs in Babel? What should a debug trace look like?

@jayphelps
Copy link
Owner

Works for me: http://react.jsbin.com/baqemeqedi/edit?js,output

Guessing your issue is elsewhere. Can you be more specific when you say it doesn't work? What happens instead?

@jayphelps
Copy link
Owner

@wmertens ping

@wmertens
Copy link
Author

Yes, sorry, need some more time to repro. The problem is that I am trying
to use it in a big project and it is entirely unclear to me how it can fail
in only this one place…

Wout.
(typed on mobile, excuse terseness)

@mike-engel
Copy link

Hey there. I'm also having this problem. For me, the issue occurs here: https://github.com/jayphelps/core-decorators.js/blob/master/src/autobind.js#L68, wherein this is equal to target. I'm not sure why this is occurring. Simplified example below.

import React, { PropTypes, Component } from 'react'
import { autobind } from 'core-decorators'
import { Button } from './custom-button-component'

export class ActivityCalendar extends Component {
  @autobind
  toggleCalendar () {
    // `this` is null when called
    this.setState({ showCalendar: true })
  }

  render () {
    return (
      <div>
        <Button cssClass='btn--input'
          onClick={this.toggleCalendar}>
          <span className='icon'></span>
        </Button>
      </div>
    )
  }
}

The Button component returns the following:

<button
  type={ this.props.type }
  onClick={ this.props.onClick }
  className={'btn ' + this.props.cssClass }>
  { this.props.children }
</button>

core-decorators: 0.11
babel: 5.8.31
react: 0.14.7

Let me know if you need any more info or I can try something.

@jayphelps
Copy link
Owner

@mike-engel your example, as provided, works for me as well.

http://react.jsbin.com/dakayo/3/edit?js,output

It's theoretically possible that some how 0.11.0 published to npm is different than the 0.11.0 global build I'm using to try and reproduce in the jsbin, but (confirmed not the case) we have numerous apps that are also on 0.11.0 and use @autobind extensively without issue so I'm at a loss.

I'm gonna create a local project with the same jsbin code using npm instead of globals just in case (done, see next comment). In the meantime can you confirm the provided example as-is reproduces the issue for you locally, without additional code that may have been omitted?

Edit: I'm absolutely not dismissing you btw. Clearly there is a problem somewhere. I'm just inclined to believe it's somehow related to a conflict between core-decorators and some other library or code that hasn't been presented to me yet. Our tests are pretty exhaustive and our @autobind covers several edge cases that no other implementation I've seen does like inheritance overrides, etc.

@jayphelps
Copy link
Owner

@mike-engel Here's a ready-to-run app that uses your example code with some very minor modifications so you can actually see feedback that it's working. https://github.com/jayphelps/core-decorators-issue-48

$ git clone https://github.com/jayphelps/core-decorators-issue-48.git
$ cd core-decorators-issue-48
$ npm install
$ npm start
$ # now open localhost:7001 in your browser

It works as expected. ¯_(ツ)_/¯ any guesses on your end?

@jayphelps
Copy link
Owner

Also of note, I could not install babel v5.8.31. It doesn't seem to be available in npm. Maybe this is somehow related? Can you confirm your version again?

$ npm view babel versions
[
  ...
  '5.8.12',
  '5.8.19',
  '5.8.20',
  '5.8.21',
  '5.8.23',
  '5.8.29',
  '5.8.34',
  '5.8.35',
  '6.0.0',
   ...
]

@mike-engel
Copy link

Thanks for putting that together @jayphelps! I'm running it now. Sorry for the babel typo, I'm on 5.8.35, which is what is installed in the example. It looks like your example is working correctly, so I'm going to slowly start adding dependencies (some of the private, which is why I kept them out), and I'll update you with my findings.

@mike-engel
Copy link

Also, FWIW, downgrading to core-decorators 0.10 doesn't solve it. So it's definitely not tied to 0.11.

@jayphelps
Copy link
Owner

@mike-engel what browser/version are you using?

@mike-engel
Copy link

Original breakage reported on Firefox 44. The demo, however, is working on both FF 44, 45, and Chrome stable.

@jayphelps
Copy link
Owner

@mike-engel In the file causing you trouble, can you remove the import of autobind and instead place this code at the top of the file and see if this simplified decorator implementation works for you. If it does, it helps narrow the problem down a bit. Keep in mind this is a naive implementation--it only works in the most basic use case.

function autobind(target, key, { value: fn }) {
  return {
    configurable: true,
    enumerable: false,

    get() {
      const boundFn = fn.bind(this);

      Object.defineProperty(this, key, {
        configurable: true,
        writable: true,
        // NOT enumerable when it's a bound method
        enumerable: false,
        value: boundFn
      });

      return boundFn;
    },
    set(newValue) {
      Object.defineProperty(this, key, {
        configurable: true,
        writable: true,
        // IS enumerable when reassigned by the outside word
        enumerable: true,
        value: newValue
      });

      return newValue;
    }
  };
}

@mike-engel
Copy link

Quick update @jayphelps: I haven't had a lot of time yet to continue looking into slowly adding code until I find the piece that breaks. I did get a couple minutes, however, to use the simple autobind function. It does bind this correctly, but for some reason this doesn't have any props.

@autobind
toggleCalendar () {
  console.log(this) // react component object
  console.log(this.props) // undefined
}

I know I should have at least some props since I have defaultProps set, and the render method uses them. Let me know if you've got anything more you'd like me to try. I'll try and keep experimenting with the first part a little more tomorrow. Thanks for your time on this, by the way!

@jayphelps
Copy link
Owner

@mike-engel inside toggleCalendar add this:

@autobind
toggleCalendar () {
  console.log(this === ActivityCalendar, this instanceof ActivityCalendar);
}

I have a feeling it isn't an instance of ActivityCalendar but instead the actual static class object of ActivityCalendar. Which means you've got some reeeeally weird stuff going on. Can you show me how you are consuming ActivityCalendar? e.g. the code where you <ActivityCalendar />

@jayphelps
Copy link
Owner

@mike-engel are you calling or accessing the property toggleCalendar anywhere else besides inside render()?

@mike-engel
Copy link

@jayphelps Results from the log are false false. Below is what we're using to render ActivityCalendar

return (
  <div className={getTaskClassName('activity')}>
    <TaskSummary task={this.props.task}
      iconClassName={getIconClassName(this.props.taskState.showTaskContent)}
      handleClick={this.handleExpandContent} />
    <Collapse isOpen={this.props.taskState.showTaskContent}>
      <div className='activities-detail'>
        {this.renderActivityHeader()}
        <div className='activities-detail__body'>
          <span className={classnames(reportClasses)} onClick={this.toggleSelfReport}>Report activity <span className='ico-caret-next'></span></span>
          <Collapse isOpen={this.props.taskState.showReportForm}>
            <ActivityLogger task={this.props.task}
              taskState={this.props.taskState}
              logEntries={this.props.log}
              dispatch={this.props.dispatch} />
          </Collapse>
        </div>
      </div>
    </Collapse>
  </div>
)

If you're wondering, Collapse is a simple wrapper that basically renders a div with children

render () {
  return (
    <div style={this.state.styles}>
      {this.props.children}
    </div>
  )
}

Just for good measure, I threw this up at the top-most react component/entry point of our app

@autobind
handleButtonClick () {
  console.log(this.props) // `this` is null
}

render () {
  return (
    <div>
      <button onClick={this.handleButtonClick}>Test click</button
      ...other element stuff...
    </div>
  )
}

This is a code base I just jumped on, so I'm still trying to figure out what's different in this one compared to ones I've used before (and where core-decorators work splendidly). As I get time, I'm going to keep digging like I mentioned earlier.

@jayphelps
Copy link
Owner

@mike-engel I don't see ActivityCalendar inside that render() block? There's ActivityLogger, perhaps you accidentally thought that was it? 😄

@jayphelps
Copy link
Owner

@mike-engel I'm happy to do a Screenhero session with you to try and really debug this in your actual codebase. I realize your company may have issues with outsiders seeing the code, but perhaps just one person might ease that concern. Happy to esign an NDA (most companies have "hired" contractors so they can treat me like one, just no need to pay me). I really want to get to the bottom of this in case this is truly something core-decorators can prevent others from hitting.

@mike-engel
Copy link

@jayphelps Sorry about that—multiple people working in the codebase === more abstractions sometimes 😄 But yes, it used to be ActivityCalendar but got renamed to a wrapper component. Edited now.

If I can't pintpoint the exact location/component/module causing this to break, I'll check around with the higher ups™ to see if I can't run through it with you. I'll try and update you by the end of the week if that's ok?

@jayphelps
Copy link
Owner

@mike-engel no rush for me, it's your problem :trollface:

@jayphelps
Copy link
Owner

@mike-engel although you didn't mention using @autobind on the entire class, I noticed that you're using redux, et al just like #56 was so perhaps it was a related issue? Give v0.11.2 a try?

@mike-engel
Copy link

@jayphelps No luck 😦

I did get some time tonight to play around, and found a couple things.

  1. Autobind stops working one component lower than the entry point
  2. The demo project and my project, even when synced on core dependencies, still don't work the same
  3. When compiling my app with our prod webpack config, it works fine.

This leads me to believe there's some webpack loader or something breaking things, and I'll try to look at it this week(end). Thanks for the heads up, and sorry for the silence.

@jayphelps
Copy link
Owner

@mike-engel if it works only with prod builds it very much suggests a babel plugin injecting things for debug, just like #56 was (but probably a different one than #56 since 0.11.2 doesn't resolve it for you)

Can you list out the babel plugins you're using? Most likely one of the various react-transforms that are popular with redux folks. Actually, just paste in all your devDependencies 😄

@jayphelps
Copy link
Owner

@wmertens still seeing this issue with v0.11.2?

@mike-engel
Copy link

@jayphelps, here are all our devDependencies:

"autoprefixer-loader": "^2.0.0",
"babel": "^5.8.25",
"babel-core": "^5.8.35",
"babel-eslint": "^4.1.3",
"babel-loader": "^5.3.2",
"babel-plugin-rewire": "^0.1.22",
"babel-polyfill": "^6.7.2",
"babel-runtime": "^5.8.25",
"chai": "^3.0.0",
"chai-enzyme": "^0.4.1",
"css-loader": "^0.16.0",
"envc": "^2.4.1",
"enzyme": "^2.0.0",
"eslint": "^0.23.0",
"eslint-plugin-react": "^3.2.3",
"exports-loader": "^0.6.2",
"express": "^4.13.3",
"extract-text-webpack-plugin": "^0.8.2",
"file-loader": "^0.8.4",
"git-rev-sync": "^1.4.0",
"imports-loader": "^0.6.4",
"jsdom": ">= 0.1.23 < 4.0.0",
"load-script": "^1.0.0",
"mocha": "^2.2.5",
"nock": "^7.2.2",
"node-bourbon": "^4.2.3",
"node-neat": "^1.7.2",
"node-sass": "^3.2.0",
"react": "^0.14.3",
"react-addons-test-utils": "0.14.6",
"react-autosuggest": "^3.5.1",
"react-collapse": "^2.0.0",
"react-dom": "^0.14.2",
"react-hot-loader": "^1.2.8",
"redux-logger": "^2.0.4",
"rootpath": "^0.1.2",
"sass-loader": "^2.0.1",
"sinon": "^1.16.0",
"sinon-chai": "^2.8.0",
"sinon-spy-react": "^1.0.4",
"standard": "^5.1.0",
"standard-loader": "^2.0.0",
"style-loader": "^0.12.3",
"swagger-express-middleware": "0.4.7",
"url-loader": "^0.5.6",
"webpack": "^1.11.0",
"webpack-dev-server": "^1.10.1"

I already tried removing the babel rewire plugin from out config and that didn't seem to be the issue. Let me know if you'd like me to try anything.

@jayphelps
Copy link
Owner

Pulling in @gaearon so he can confirm my findings and suggestions.

@mike-engel Thank you for providing your deps, found your issue using them and I'm betting it's the same one @wmertens has since it is with react-hot-loader's react-hot-api dependency, I believe both of you are using.

@wmertens can you read this as well and see if it applies to you? (if you use react-hot-reloader, it does)

It was fixed here but basically it is the way they are looking up and invoking the end method, basically proxying but doing so in a brittle way that loses the @autobind wrapper so this ends up being "correctly" null since the function was in fact not bound.

react-hot-loader appears to be deprecated but there was some work that includes the version of react-hot-api with the fixed behavior, v0.5.0. I installed the alpha version via [email protected] and even though it complains about needing babel-core@^6.0.0 (you are using babel@5) it still appeared to work for me and solve the issue as expected.

SO...in summary, there isn't anything I'm aware of that I can do to mitigate this on my end because we need to support looking up the original property without autobind coming along. i.e. Klass.prototype.myBoundMethod needs to return the original method, not the bound wrapper which we explicitly handle. Why? Because autobinding to the prototype makes no sense and several people reported that as a bug prior.

You have a couple options I think:

  • Patch your current of version of react-hot-loader to use [email protected]
  • Upgrade to [email protected] at the risk of using alpha software and either ignore the babel@6 warnings or upgrade and use babel-plugin-transform-decorators-legacy for decorator support.
  • Upgrade to react-transform + react-transform-hmr etc, the replacement of react-hot-loader which I imagine doesn't have this issue, but I can't confirm that at the moment. Briefly digging into how it works, it appears much more intelligent about how it proxies.

If someone can think of a way for core-decorators to maintain its existing behavior but still lazy autobind in this case, I'm definitely willing to explore it but I racked my brain and can't see how.

@jayphelps jayphelps changed the title @autobind doesn't work sometimes @autobind doesn't work correctly with react-hot-loader <= 1.3.0 Mar 24, 2016
@mike-engel
Copy link

@jayphelps First off, thanks so much for your time and effort looking into this! This was a project I inherited so apologies for not being more helpful 😅

Second, I'm totally ok with either updating react-hot-loader or moving to a more modern implementation of hot loading react code. No need to worry about this one edge case, especially if it's with a deprecated third party module.

As far as I'm concerned, this issue can be closed and I'll work independently on getting our deps updated (fat arrow functions are working for now).

Thanks again for the help, and thanks for publishing this module. I've found it super useful and I know my team has too.

@wmertens
Copy link
Author

@jayphelps wow, thanks for finding this! I will try your solutions today and report back. Sorry I have not been more responsive, swamped with work :-(

@raphaelvigee
Copy link

I'm using react-hot-loader ^1.3.1 and core-decorators ^0.15.0, the autobind decorator is not working...

@jayphelps
Copy link
Owner

jayphelps commented Feb 11, 2017

@raphaelvigee They released 1.3.1 since this thread was originally created, but it did not include the described fix they needed. They have also deprecated react-hot-loader.

I'm not aware of a way for core-decorators to workaround this--remember, this is due to the way react-hot-loader is patching methods (technically their dependency).

Wish I could help, but don't know how I can.

@wmertens
Copy link
Author

I just tested this with the react hot loader 3 beta and I can confirm that it is solved there.

@jayphelps since this really not core-decorator's issue, shall I (or you) close this? Or would you like to keep it open for visibility?

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

No branches or pull requests

4 participants