document is not defined when attempting to setState from the return of an async call in componentWillMount

前提是你 提交于 2019-12-09 14:54:47

问题


I grab my data in my componentWillMount call of my component [actually it's in a mixin, but same idea]. After the ajax call returns, I attempt to setState, but I get the error that the document is not defined.

I'm not sure how to get around this. Is there something to wait for? A promise, or callback I should be doing the setState in?

This is what I'm trying to do:

componentWillMount: function() {
    request.get(this.fullUrl()).end(function(err, res) {
        this.setState({data: res.body});
    }.bind(this));
}

回答1:


I've actually encountered a similar situation before. I assume the error you encountered was something like this:

Uncaught Error: Invariant Violation: replaceState(...): Can only update a mounted or mounting component.

The error is caused by the fact that, in React components, you cannot set state before the component is mounted. So, instead of attempting to set the state in componentWillMount, you should do it in componentDidMount. I typically add an .isMounted() check, just for good measure.

Try something like this:

componentDidMount: function () {
  request.get(this.fullUrl()).end(function(err, res) {
    if (this.isMounted()) {
      this.setState({data: res.body});
    }
  }.bind(this));
}


EDIT: Forgot to mention ... If the component gets "unmounted" before the async operation completes, you may also encounter an error.

This can be easily handled if the async operation is "cancelable". Assuming your request() above is something like a superagent request (which are cancelable), I would do the following to avoid any potential errors.

componentDidMount: function () {
  this.req = request.get(this.fullUrl()).end(function(err, res) {
    if (this.isMounted()) {
      this.setState({data: res.body});
    }
  }.bind(this));
},

componentWillUnmount: function () {
  this.req.abort();
}


EDIT #2: In one of our comments you mentioned your intent was to create an isomorphic solution that could load state asynchronously. While this is beyond the scope of the original question, I will suggest you check out react-async. Out-of-the-box, it provides 3 tools that can help you achieve this goal.
  1. getInitialStateAsync - this is provided via a mixin, and it allows a component to fetch state data asyncrhonously.

    var React = require('react')
    var ReactAsync = require('react-async')
    
    var AwesomeComponent = React.createClass({
      mixins: [ReactAsync.Mixin],
    
      getInitialStateAsync: function(callback) {
        doAsyncStuff('/path/to/async/data', function(data) {
          callback(null, data)
        }.bind(this))
      },
    
      render: function() {
         ... 
      }
    });
    
  2. renderToStringAsync() - which allows you to render server side

    ReactAsync.renderToStringAsync(
      <AwesomeComponent />,
      function(err, markup, data) {
        res.send(markup);
      })
    );
    
  3. injectIntoMarkup() - which will inject the server state, along with the markup to ensure it's available client-side

    ReactAsync.renderToStringAsync(
      <AwesomeComponent />,
      function(err, markup, data) {
        res.send(ReactAsync.injectIntoMarkup(markup, data, ['./app.js']));
      })
    );
    

react-async provides far more functionality than this. You should check out the react-async documentation for a full list of its features, and a more comprehensive explanation of the ones I briefly describe above.




回答2:


It's not a good idea to be doing something asynchronous inside componentWillMount. You should really be doing this in the componentDidMount because if the first task a component does is to fetch some data - you're probably going to want it to show a spinner or some kind of loading notifier before it gets that data.

As a result I personally don't ever do what you're doing, opting for componentDidMount every time. Then you can set your initial state so that that first mounting render shows a loading spinner or some other kind of initial state to the user. The ajax fires, and you update once you get a response. This way you know that you're handling cases where your user is on a crappy connection, such as mobile with bad reception or such, giving a good UX by letting the user know that a component is loading some data which is why they don't see anything yet.

This all being said, why do you get an error when performing some asynchronous functions within componentWillMount - because if you just called this.setState inside the lifecycle function itself, it would work fine right? This is down to a quirk of how React works, it's been around since at least React 0.11 as far as I'm aware. When you mount a component, executing this.setState synchronously inside componentWillMount works just fine (although there's a bug in 0.12.x where any function callback passed to setState inside componentWillMount will not be executed). This is because React realises that you're setting the state on a component which isn't yet mounted - something that you can't usually do - but it allows it within lifecycle functions like componentWillMount specially. However when you asynchronize that setState call, it's no longer treated specially and the normal rules apply - you cannot setState on a component which is not mounted. If your ajax request returns very quickly, it's entirely possible that your setState call is happening AFTER the componentWillMount phase but BEFORE the component has actually mounted - and you get an error. If in fact your ajax request wasn't as fast as it evidently is, say it took a second or more, then you probably wouldn't notice an error since it's highly likely that your component mounted fully within a second and so your setState call becomes valid by normal rules again. But you're basically giving yourself a race condition here, be safe and use componentDidMount instead - as it's also better for other reasons I talked about above.

Some people are saying you can do this inside a setTimeout and they are correct, but it's basically because you're increasing the time taken for your request to a minimum of x, which is usually enough to force it to execute setState AFTER the component has mounted - so effectively you might as well have been doing your setState inside componentDidMount instead and not rely on the component mounting within your setTimeout timer.

TL;DR answer:

You can setState inside componentWillMount synchronously, although it's not recommended. Ideally any situation where you do this synchronously, you would use getInitialState instead.

However using setState asynchronously in componentWillMount is extremely unwise as it will open you to potential race conditions based on the time your async task takes. Use componentDidMount instead and use that initial render to show a loading spinner or similar :)




回答3:


Trying this out in a simple component, the following works just fine:

  getInitialState: function() {
    return {
      title: 'One'
    };
  },

  componentWillMount: function() {
    setTimeout(function(){
      this.setState({
        title: 'Two'
      });
    }.bind(this), 2000);
  },

Can you post the exact error, and perhaps the stacktrace, so we may better see the problem you are having?



来源:https://stackoverflow.com/questions/29782884/document-is-not-defined-when-attempting-to-setstate-from-the-return-of-an-async

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!