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

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__;
    }()
  }

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 58
  • Comments: 30

Commits related to this issue

Most upvoted comments

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

+1

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 found another workaround:

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

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()} />
    )
  }
}

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

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

Confirm, it’s fixed in v4

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

@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.

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 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()} />
    )
  }
}

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?

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

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’re not imported anywhere else.

@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.

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?

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