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));
}
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.
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() {
...
}
});
renderToStringAsync()
- which allows you to render server side
ReactAsync.renderToStringAsync(
<AwesomeComponent />,
function(err, markup, data) {
res.send(markup);
})
);
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.
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 :)
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?