Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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));
}
like image 547
Sophie McCarrell Avatar asked Apr 21 '15 20:04

Sophie McCarrell


People also ask

How do I use setState in componentWillMount?

setState() in componentWillMount()componentWillMount() is invoked immediately before mounting occurs. It is called before render() , therefore setting state in this method will not trigger a re-render. Avoid introducing any side-effects or subscriptions in this method.

Why is setState undefined?

The "cannot read property 'setState' of undefined" error occurs when a class method is called without having the correct context bound to the this keyword. To solve the error, define the class method as an arrow function or use the bind method in the classes' constructor method.

Can we make component did mount async?

yes. Here is a good article which shows why async await is a good option over promises hackernoon.com/…


2 Answers

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

like image 74
Mike Driver Avatar answered Nov 01 '22 07:11

Mike Driver


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.

like image 24
JME Avatar answered Nov 01 '22 07:11

JME