React-hot-loader: Invalid 'this' binding with async arrow function class properties

Created on 5 Oct 2016  路  30Comments  路  Source: gaearon/react-hot-loader

Async arrow functions use an invalid 'this' binding when they appear as class properties, as long as the react-hot-loader/babel plugin is present at all.

Arrow functions transformed by react-hot-loader also seem to not respect the spec setting given to the transform-es2015-arrow-functions plugin; however setting spec to false does not help avoid the invalid code either.

I've added some snippets of the problematic code, and also some comments annotating both the input and the output.

class Scroller { // not a React component
  /* ... */
  loadMore = async () => {
    const { data } = await api.getAsync(this.nextUrl, null)
    this.nextUrl = data.next_url
    this.appendData(data)
  }

  // written to verify if it only affected async
  loadMorePromise = () => {
    api.getAsync(this.nextUrl, null)
    .then(({ data }) => {
      this.nextUrl = data.next_url
      this.appendData(data)
    })
  }
}

Snippets of the generated code; in the constructor:

    this.loadMorePromise = function () {
      // _newArrowCheck and .bind(this) added by the spec: true option
      _newArrowCheck(this, _this);

      return this.__loadMorePromise__REACT_HOT_LOADER__.apply(this, arguments);
    }.bind(this);

    this.loadMore = function () {
      _newArrowCheck(this, _this);

      var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() {
        var _args2 = _arguments;
        return regeneratorRuntime.wrap(function _callee2$(_context2) {
          while (1) {
            switch (_context2.prev = _context2.next) {
              case 0:
                _context2.next = 2;
                return _this.__loadMore__REACT_HOT_LOADER__.apply(_this, _args2);

              case 2:
                return _context2.abrupt('return', _context2.sent);

              case 3:
              case 'end':
                return _context2.stop();
            }
          }
        }, _callee2, _this);
      }));

      // wrong Function.name; this one is probably a babel bug
      return function NAME(_x2) {
        return _ref2.apply(this, arguments);
      };
    }.bind(this)();

In the _createClass helper's argument list:

  {
    key: '__loadMorePromise__REACT_HOT_LOADER__',
    value: function __loadMorePromise__REACT_HOT_LOADER__() {
      var _this2 = this;

      // why is this arrow function not using .bind and _newArrowCheck?
      api.getAsync(this.nextUrl, null).then(function (_ref6) {
        var data = _ref6.data;

        _this2.nextUrl = data.next_url;
        _this2.appendData(data);
      });
    }
  }, {
    key: '__loadMore__REACT_HOT_LOADER__',
    value: function () {
      var _ref7 = _asyncToGenerator(regeneratorRuntime.mark(function _callee5() {
        var _ref8, data;

        return regeneratorRuntime.wrap(function _callee5$(_context5) {
          while (1) {
            switch (_context5.prev = _context5.next) {
              case 0:
                _context5.next = 2;
                return api.getAsync(_this5.nextUrl, null);

              case 2:
                _ref8 = _context5.sent;
                data = _ref8.data;

                // _this5 was not defined anywhere, runtime crash
                _this5.nextUrl = data.next_url;
                _this5.appendData(data);

              case 6:
              case 'end':
                return _context5.stop();
            }
          }
        }, _callee5, this);
      }));

      function __loadMore__REACT_HOT_LOADER__() {
        return _ref7.apply(this, arguments);
      }

      return __loadMore__REACT_HOT_LOADER__;
    }()
  }
bug

Most helpful comment

This is known bug. A solution was found and will come in next version.

All 30 comments

I am also getting this error, when I remove react-hot-loader, babel compiles as expected

It should be a troubling bug to me, too.

Is it possible to disable hot loader for component/method that fails?

@valerymercury not currently. it might be worth adding an option to the Babel plugin to opt out of class properties transform.

@Kovensky: I took a look at this, and I think it's two things: there's a problem with the RHL plugin, where we unnecessarily add async/await to the generated class property. I also think this Babel bug is part of the issue, since we use rest params in the generated code.

So I was working on a potential fix on calesce/async-fix.

Weirdly enough, if I just run the plugin ahead of time without other Babel transforms and copy the output (using astexplorer), the code runs fine. But if I run it as a normal plugin, it has the broken _this behavior. In both instances I'm using es2015 and stage-2 presets.

Reproduce project here, I'm kind of stuck on this right now. Anyone else have any ideas?

Oddly enough, removing the react-hot-loader/babel plugin from my .babelrc file fixed this problem.

I no longer get the invalid this binding problem on async arrow functions and my react modules hot reload as expected.

@danielarias123 yeah, but we don't want people to have to remove react-hot-loader/babel because then stateful components don't hot-reload :smile:

@calesce hmmm that's weird because both stateless functions that return jsx and stateful React classes are hot reloading for me when the plugin is removed.馃

@danielarias123 Right, but they won't retain their state and will remount, you can check by adding componentWillUnmount and see it get called on save.

I've found that the order of the plugins in your .babelrc makes a huge difference. For example, the following produces this error:

plugins: [
  'react-hot-loader/babel',
  'transform-regenerator',
]

This also gives the error:

plugins: [
  'transform-regenerator',
],
env: {
  development: {
    plugins: [
      'react-hot-loader/babel',
    ],
  },
}

However, this works fine:

plugins: [
  'transform-regenerator',
  'react-hot-loader/babel',
]

I managed to find the root cause of the problem as it appeared for us too.

The problem is strictly connected to the copy of arrow function created as a method in a plugin. However, it's OK when there's no async there.

I assume there's a problem with babel itself rather than methods used there, but there's a part of code I found harmful for this case: https://github.com/gaearon/react-hot-loader/blob/51dae3f4c86f21c143db838ff5d880433e4bc739/src/babel/index.js#L206

I also created an issue with more details on babel's repo: https://github.com/babel/babel/issues/5078

And here is the reproduction repository: https://github.com/wkwiatek/babel-async-test

BTW: @calesce, do you need some help on the v3 milestone?

@wkwiatek is this fixed now? I got this error too. How can I workaround it?

@mqklin same here, have you found a workaround?

This doesn't work:

class App extends React.Component {
  myAsyncMethod = async () => {
    return new Promise((resolve) => resolve(true))
  }
  render() {
    return (
      <Button onPress={this.myAsyncMethod} />
    )
  }
}

However, this works fine with:

class App extends React.Component {
  async myAsyncMethod() {
    return new Promise((resolve) => resolve(true))
  }
  render() {
    return (
      <Button onPress={() => this.myAsyncMethod()} />
    )
  }
}

@andreatosatto90 no, I still use v2 :( I tried this https://github.com/gaearon/react-hot-loader/issues/391#issuecomment-268638968 but it causes state reload - https://github.com/gaearon/react-hot-loader/issues/642, so I'm just waiting for updates on this issue.

I found another workaround:

class App extends React.Component {
  myAsyncMethod = () => async () => {
    return new Promise((resolve) => resolve(true))
  }
  render() {
    return (
      <Button onPress={this.myAsyncMethod()} />
    )
  }
}

This issue is not limited to async functions. https://github.com/gaearon/react-hot-loader/issues/554

I am escaping this issue with:

class App extends React.Component {
  myAsyncMethod = async () => {
    const _ = arguments // eslint-disable-line
    return new Promise((resolve) => resolve(true))
  }
  render() {
    return (
      <Button onPress={this.myAsyncMethod} />
    )
  }
}

or using autobind-decorator

class App extends React.Component {
  @autobind
  async myAsyncMethod() {
    return new Promise((resolve) => resolve(true))
  }
  render() {
    return (
      <Button onPress={this.myAsyncMethod} />
    )
  }
}

+1

It seems that using the alternative config by removing _react-hot-loader/babel_ from .babelrc and adding _react-hot-loader/webpack_ to webpack.config.js works. It is not the same thing but it is good enough for me.

Source: http://gaearon.github.io/react-hot-loader/getstarted/

Note: react-hot-loader/webpack only works on exported components, whereas react-hot-loader/babel picks up all top-level variables in your files. As a workaround, with Webpack, you can export all the components whose state you want to maintain, even if they鈥檙e not imported anywhere else.

.. that means that babel transformation, to support arrow functions can be removed!? Let me double check. It should not work.

FYI (Blindly) downgrading from 3.0.0+ to 3.0.0-beta.7 worked for me.

some problem when upgrade react-hot-loader, in async arrow function, 'this' is undefined.
I change foo = async () => { } to async foo() { }, and it works.

This is known bug. A solution was found and will come in next version.

FYI (Blindly) downgrading from 3.0.0+ to 3.0.0-beta.7 worked for me.

Can confirm also worked for me.

This issue in v3 (also along with #650) is super frustrating, good to hear next version will be fixing things but right now am stuck on an old version with all warnings suppressed and HMR basically not working properly -- on an otherwise very vanilla create-react-app babel config.

I found that running a react-hot-loader pass completely separate from a babel pass with all other plugins avoided these issues, even if I edited the react-hot-loader code to make it not skip transforming arrow functions.

The problem seems to happen when it interacts with other transforms; something is executing out of order and changing things before react-hot-loader gets to handle them. At first I thought it was related to mutating nodes and not properly t.cloneing them, but no matter how much cloning was done the result code was still broken.

I then tried printing the node that react-hot-loader was trying to transform to begin with, and it had already been modified by other transforms. Maybe react-hot-loader/babel needs to do all its changes as a sub-traversal on Program enter.

This would be definitely fixed in v4 since we do not transpile arrow functions any more.

Confirm, it's fixed in v4

I found a workaround that doesn't make you to change any code except the method declaration. Just wrap it in IIFE:

class App extends React.Component {
  myAsyncMethod = (() => async () => {
    return new Promise((resolve) => resolve(true))
  })()

  render() {
    return (
      <Button onPress={this.myAsyncMethod()} />
    )
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

reintroducing picture reintroducing  路  4Comments

adesmet picture adesmet  路  4Comments

Opty1712 picture Opty1712  路  4Comments

theKashey picture theKashey  路  4Comments

theKashey picture theKashey  路  3Comments