Reputation: 585
I have a problem with one of my React components. I think AJAX doesn't load all the content from external server before React renders the ChildComp
component.
Above you can see the tree of response which is coming from server. And this is my component's code:
var ChildComp = React.createClass({
getInitialState: function(){
return {
response: [{}]
}
},
componentWillReceiveProps: function(nextProps){
var self = this;
$.ajax({
type: 'GET',
url: '/subscription',
data: {
subscriptionId: nextProps.subscriptionId //1
},
success: function(data){
self.setState({
response: data
});
console.log(data);
}
});
},
render: function(){
var listSubscriptions = this.state.response.map(function(index){
return (
index.id
)
});
return (
<div>
{listSubscriptions}
</div>
)
}
});
This is working just fine, but if I change my return to:
return (
index.id + " " + index.order.id
)
id is undefined. And all of the other properties of order object. Why is it like this? If I console.log
my response after success
function it gives all the data (like in picture). My guess is that only the first objects are loaded when React renders the component and after that all other inner objects are loaded. I can't really say if that's the case (sounds so weird) nor I can't say how to solve this out.
I tried also something like this
success: function(data){
self.setState({
response: data
}, function(){
self.forceUpdate();
})
}.bind(this)
but still re-render happens before my console.log(data)
is triggered. How to wait for AJAX response and only after that render the component?
Upvotes: 39
Views: 54543
Reputation: 1
Just do the AJAX call in componentDidMount()
and in render you can simply check...
if(this.state.response === '' || this.state.response === [] || this.state.response === undefined)
return <Loader />
else{
// do your stuff here
}
OR if you are using react 16.6 or more, you can simply use lazy components.
Upvotes: 0
Reputation: 92200
Here's your code reworked with some comments of the modified parts
getInitialState: function(){
return {
// [{}] is weird, either use undefined (or [] but undefined is better).
// If you use [], you loose the information of a "pending" request, as
// you won't be able to make a distinction between a pending request,
// and a response that returns an empty array
response: undefined
}
},
loadSubscriptionData: function(subscriptionId){
var self = this;
// You probably want to redo the ajax request only when the
// subscriptionId has changed (I guess?)
var subscriptionIdHasChanged =
(this.props.subscriptionId !== subscriptionId)
if ( subscriptionIdHasChanged ) {
// Not required, but can be useful if you want to provide the user
// immediate feedback and erase the existing content before
// the new response has been loaded
this.setState({response: undefined});
$.ajax({
type: 'GET',
url: '/subscription',
data: {
subscriptionId: subscriptionId //1
},
success: function(data){
// Prevent concurrency issues if subscriptionId changes often:
// you are only interested in the results of the last ajax
// request that was made.
if ( self.props.subscriptionId === subscriptionId ) {
self.setState({
response: data
});
}
}
});
}
},
// You want to load subscriptions not only when the component update but also when it gets mounted.
componentDidMount: function(){
this.loadSubscriptionData(this.props.subscriptionId);
},
componentWillReceiveProps: function(nextProps){
this.loadSubscriptionData(nextProps.subscriptionId);
},
render: function(){
// Handle case where the response is not here yet
if ( !this.state.response ) {
// Note that you can return false it you want nothing to be put in the dom
// This is also your chance to render a spinner or something...
return <div>The responsive it not here yet!</div>
}
// Gives you the opportunity to handle the case where the ajax request
// completed but the result array is empty
if ( this.state.response.length === 0 ) {
return <div>No result found for this subscription</div>;
}
// Normal case
var listSubscriptions = this.state.response.map(function(index){
return (
index.id
)
});
return (
<div>
{listSubscriptions}
</div>
)
}
Upvotes: 31
Reputation: 12966
I threw it into a JSBin and I can't seem to reproduce your problem.
http://jsbin.com/jefufe/edit?js,output
Every time the button is clicked, it sends new props to <ChildComp/>
, which then triggers its componentWillReceiveProps
, sends the (fake) ajax request, receives the (fake) response, sets the response state to the (fake) response, and re-renders the component with the correct data shown.
The only thing I added was a check for index.id == null
before trying to access it, but I don't think that does anything to solve the issue you're having about being unable to access index.order.id
.
The only reason I can come up with as to why you can't access index.order.id
is because perhaps you have some code that you aren't showing us that modifies the response at some point.
Also, remember that React components will always re-render when its state or props have changed, unless you have defined behavior in shouldComponentUpdate
that says otherwise. This means that when you receive the whole object from the server and set it to a state in your component, it will re-render and show you the updated state. There is no reason/way for React to "shallowly render" your response object like you were suggesting in your original post.
I'm not sure if this answer helps, but hopefully you can look at the JSBin I provided and find something that you might have overlooked.
Upvotes: 0
Reputation: 2577
First of all, you would want to put the AJAX call inside either componentWillMount()
or componentDidMount()
(if you need to do some DOM manipulation).
componentWillReceiveProps()
is
Invoked when a component is receiving new props. This method is not called for the initial render.
But even if you put the AJAX call inside componentWillMount()
or componentDidMount()
, render()
will probably be called with an empty response before AJAX call finishes.
So the order of call would be like this (I assumed you put the AJAX call inside componentWillMount()
:
componentWillMount()
(AJAX call is triggered) -> componentDidMount()
-> render()
(render with empty response; so index.order.id
will cause an error) -> AJAX call returns and calls this.setState()
-> (some other component lifecycle methods such as shouldComponentUpdate()
is called; see the FB link above for details) -> render()
is called again.
So the solution to your problem is:
1) Moving the AJAX call to either componentWillMount()
or componentDidMount()
2) Either set the initial state to just []
(as opposed to [{}]
) or check if index
is empty in
var listSubscriptions = this.state.response.map(function(index){
return (
index.id
)
});
Upvotes: 3