Search code examples
reactjsnode-request

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


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

Solution

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