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

3

11

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));
}
Bailsman answered 21/4, 2015 at 20:51 Comment(3)
@WiredPrairie: OP uses .bind, so that should work. @OP: Since you are not using document, I don't really understand where the error is supposed to come from.Brunildabruning
It's a reactjs thing. I'll post the error in a bit. The error has no reference to my code, just a long chain in ReactJS's code base. (Although im now newbie in JS, so I should learn to look at other peoples source code more to solve my own problems =)Bailsman
Also, as Felix said, this isn't an issue with 'this', because I'm using bind correctly. So the current duplicate question suggestion is incorrect.Bailsman
R
5

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.

Royroyal answered 29/4, 2015 at 1:33 Comment(4)
Good advice, and currently I am using componentDidMount while I await an answer. The tricky part for me is that I'm trying to write an isomorphic app that renders on the server side and only updates on the client side. If I use componentWillMount, then the server side well run it, if I use componentDidMount, then only the client side runs it, and my server side doesn't get to render the content. This is more a performance and nuance issue. I've never seen any input lag or anything, but I'm aiming for the ideal Isomorphic rendering, which is server side.Bailsman
Wasn't aware your intent was to render server-side, since there's no mention of it in your original question. Now I get why you were attempting to use componentWillMount. I know I had a similar use-case recently. Let me dig it up and I'll amend my answer.Royroyal
@JasonMcCarrell, I found two use-cases in which I needed to set state asynchronously on a react component. In each I used a different approach. In the first, I used module by Andrey Popp called react-async. I will updated my answer with a short example. In the second use-case, react-async didn't provide everything I needed, so I create a solution using flux stores. Since this last solution is way beyond the scope of this question I will not elaborate here.Royroyal
isMounted is considered an anti-patternEverything
R
6

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 :)

Redraft answered 1/5, 2015 at 21:1 Comment(1)
Very good information. Perhaps what I am doing is fundamentally incorrect. I'm hoping to render on the server side before delivering the html. I noticed that componentDidMount never triggers on the server side, but componentWillMount does.Bailsman
R
5

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.

Royroyal answered 29/4, 2015 at 1:33 Comment(4)
Good advice, and currently I am using componentDidMount while I await an answer. The tricky part for me is that I'm trying to write an isomorphic app that renders on the server side and only updates on the client side. If I use componentWillMount, then the server side well run it, if I use componentDidMount, then only the client side runs it, and my server side doesn't get to render the content. This is more a performance and nuance issue. I've never seen any input lag or anything, but I'm aiming for the ideal Isomorphic rendering, which is server side.Bailsman
Wasn't aware your intent was to render server-side, since there's no mention of it in your original question. Now I get why you were attempting to use componentWillMount. I know I had a similar use-case recently. Let me dig it up and I'll amend my answer.Royroyal
@JasonMcCarrell, I found two use-cases in which I needed to set state asynchronously on a react component. In each I used a different approach. In the first, I used module by Andrey Popp called react-async. I will updated my answer with a short example. In the second use-case, react-async didn't provide everything I needed, so I create a solution using flux stores. Since this last solution is way beyond the scope of this question I will not elaborate here.Royroyal
isMounted is considered an anti-patternEverything
A
2

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?

Admit answered 28/4, 2015 at 23:3 Comment(1)
I should have thought of doing that to narrow down my issue. Perhaps later this evening I'll do more troubjleshooting and edit in a stacktrace. Thanks for the suggestion.Bailsman

© 2022 - 2024 — McMap. All rights reserved.