React: State does not get updated after AJAX call - javascript

I'm trying to do two AJAX calls in my React project and have my UI render according to the data received. This is my render method:
render() {
if (this.state.examsLoaded) {
return (
<div>
<Button onClick={this.openModal}>Details</Button>
<Modal show={this.state.modalOpen} onHide={this.closeModal}>
<Modal.Header closeButton>
<Modal.Title>{this.props.course.name}</Modal.Title>
</Modal.Header>
<Modal.Body>
<DetailModalContent course={this.props.course} exams={this.exams} grades={this.grades}/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeModal}>Sluiten</Button>
</Modal.Footer>
</Modal>
</div>
)
}
else {
return (
<div>Loading...</div>
)
}
}
The render method checks if the AJAX data is available yet and if not, just renders a 'Loading...' message. This is the code that fetches the data:
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
});
});
},
The weird thing is, I used to only have 1 fetch method and everything would work fine. As soon as I called setState the component rerenders and the data is displayed. However, after adding the second one, it doesn't work anymore. See my console.log's. Everything works fine 'till I parse the JSON, after that, nothing gets run anymore.
What am I doing wrong?
Thanks!

fetch's json() method returns a promise. You are using it correctly in the first call, but the second call you are treating it as a function rather than a promise.
Try
gradeResponse.json().then((gradeData) => {
...
});

You need to write this logic inside componentDidUpdate. componentDidMount will be triggered only for the first time.
Please refer to the React documentation.
Probably you will need both componentDidMount and componentDidUpdate.
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
this.setState({
examsLoaded: true
}); //At this point some state is changed, so componentDidUpdate will be triggered. Then in that function below, grades will be fetched and state is changed, which should call render again.
});
});
},
componentDidUpdate(){
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
}
Since I am not with react environment right now. Will update as soon as I try.

Related

Get updated data from database with reactjs, axios and php

I want to get the latest data from a database and render it to my page.
When i load the page for the first time it works fine, but when i change data in the db nothing changes on the page. Even when i call the update function it does not change anything.
The page im posting to has a php function which retrieves data from the db (mysql).
If im doing something wrong, what would be my best option for this?
class Car extends React.Component {
constructor(props) {
super(props);
this.state = {resultState: "red"};
};
componentDidMount() {
axios.post(`https://web.site/page.php`)
.then(res => {
console.log(res.data);
this.setState({ resultState: res.data });
});
}
update() {
this.forceUpdate();
};
render() {
return (<div>
<div className="row" dangerouslySetInnerHTML={{__html: this.state.resultState}}></div>
</div>)
}
}
ReactDOM.render(<Car />, document.getElementById('mydiv'))
Overall, your code works only if you refresh your page.
A simple solution would be pooling the PHP server. Create a load function whose job is to call the PHP server and in your ComponentDidMount function do the following updates
load() {
axios.post(`https://web.site/page.php`)
.then(res => {
console.log(res.data);
// compare if the data has new values
// if it has then update the state i.e. this.setState({ resultState: res.data });
// otherwise do nothing
});
}
componentDidMount() {
this.load();
// This will call the PHP server every 1 minute
setTimeout(this.load(), 1000);
}
Do not use this.forceUpdate(); as it's not recommended by the react team to be used.

react setstate not rendering until callback finishes

I am trying to change button to saving state while I run code to get information.
I have
this.setState({ saving: true }, () => this.save(event) })
In this.save I have a rest call. I can see from the log that the state is updated but visually on the site the button does not go into the spinning circle like it should with that updated value.
Is there a way to force update rendering before running the callback function or a better method to set a button to saving while I do a remote call that could take a little bit of time?
There is no reason to force this. Change your state in parallel to the actual saving:
<button onClick={() => this.save()}>save</button>
paired with:
save() {
this.setState({ saving: true });
remoteAPI.save({
data: this.getSaveData(),
credentials: this.getCredentials()
...
}, response => {
this.setState({ saving: false });
if(response.error) {
// ohnoes!
} else {
// nice.
}
});
}

React - Loading Stored Data then API data in ComponentWillReceiveProps

I have a component that must make an HTTP request based off new props. Currently it's taking a while to actually update, so we've implemented a local store that we'd like to use to show data from past requests and then show the HTTP results once they actually arrive.
I'm running into issues with this strategy:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.setState({data:this.makeHttpRequest(nextProps.dataToGet)});
//triggers single render, only after request gets back
}
What I think is happening is that react bundles all the setstates for each lifecycle method, so it's not triggering render until the request actually comes back.
My next strategy was this:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.go=true;
}
componentDidUpdate(){
if(this.go){
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
}
this.go=false;
}
//triggers two renders, but only draws 2nd, after request gets back
This one SHOULD work, it's actually calling render with the localstore data immediately, and then calling it again when the request gets back with the request data, but the first render isnt actually drawing anything to the screen!
It looks like react waits to draw the real dom until after componentDidUpdate completes, which tbh, seems completely against the point to me.
Is there a much better strategy that I could be using to achieve this?
Thanks!
One strategy could be to load the data using fetch, and calling setState when the data has been loaded with the use of promises.
componentWillRecieveProps(nextProps){
this.loadData(nextProps)
}
loadData(nextProps){
// Create a request based on nextProps
fetch(request)
.then(response => response.json())
.then(json => this.setState({updatedValue: json.value})
}
I use the pattern bellow all the time (assuming your request function supports promises)
const defaultData = { /* whatever */ }
let YourComponent = React.createClass({
componentWillRecieveProps: function(nextProps) {
const that = this
const cachedData = this.getDataFromLocalStore(nextProps)
that.setState({
theData: { loading: true, data: cachedData }
})
request(nextProps)
.then(function(res) {
that.setState({
theData: { loaded: true, data: res }
})
})
.catch(function() {
that.setState({
theData: { laodingFailed: true }
})
})
},
getInitialState: function() {
return {
theData: { loading: true, data: defaultData }
};
},
render: function() {
const theData = this.state.theData
if(theData.loading) { return (<div>loading</div>) } // you can display the cached data here
if(theData.loadingFailed) { return (<div>error</div>) }
if(!theData.loaded) { throw new Error("Oups") }
return <div>{ theData.data }</div>
}
)}
More information about the lifecycle of components here
By the way, you may think of using a centralized redux state instead of the component state.
Also my guess is that your example is not working because of this line:
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
It is very likely that makeHttpRequest is asynchronous and returns undefined. In other words you are setting your data to undefined and never get the result of the request...
Edit: about firebase
It looks like you are using firebase. If you use it using the on functions, your makeHttpRequest must look like:
function(makeHttpRequest) {
return new Promise(function(resolve, reject) {
firebaseRef.on('value', function(data) {
resolve(data)
})
})
}
This other question might also help

React component get request being made in one or clicks late

This one is kind of hard to explain, but basically when a click on a component, I make a get request for some data for another component. This however is only made after a couple of clicks.
I should probably also admit that I am not 100% sure if the place I am making the request is even correct, so if that's the case please let me know how I can get that fixed. Here's the code:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
render: function () {
var _this=this;
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
_this.state.content = log;
}.bind(this));
return (
<div>
{_this.state.content[0]}
</div>
);
}
});
window.ChangeLog = React.createClass({
render: function () {
return (
<div>
<ChangeLogData name={this.props.params.name}
source={currentUrl + "/changelog/" +
this.props.params.name}/>
</div>
);
}
});
Edit: I should also probably add that it seems that most people recommend doing http requests on componentWillMount, but if I do that, the request only works once.
Edit 2: Here is the code of where the event is being called:
var AboutItem = React.createClass({
render: function () {
return (
<ListGroup>
{this.props.list.map(function (listValue,key) {
var link = currentUrl + "/changelog/" + listValue.split(' ')[0];
return <ListGroupItem key={key} className="module"
bsStyle="warning">
{listValue}
</ListGroupItem>
})}
</ListGroup>
);
}
});
I guess the idea is, the user will click on an item (that is dynamically generated), and when the item is clicked, it will send to the ChangeLog component the data in which it has to do the get request. So where exactly would I put my event handler?
I think the problem is that it's not being called correctly, as jquery is async...
var jqxhr = $.get( "example.php", function() {
alert( "success" );
})
.done(function() {
// PUT YOUR CALLBACK CODE HERE WITH THE RESULTS
})
.fail(function() {
alert( "error" );
})
.always(function() {
alert( "finished" );
});
And update the state in the .done()
You should not be making requests in your render method. You should also not be directly modifying state through this.state but instead use this.setState(). You also don't seem to be adding any onClick handlers.
You can do something like the following to trigger requests onClick:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
handleClick: function() {
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
this.setState( { content: log } );
}.bind(this));
}
render: function () {
return (
<div onClick = { this._handleClick } >
{_this.state.content[0]}
</div>
);
}
If you want to do it on component mount you can put into componentDidMount() and call setState when you retrieve your data. This will cause the component to re-render with content
First: the get request is async, so by the time you get the response back the DOM is already rendered.
Second: Never update state inside the render method, if it works and you don't get an error message you most likely will create an infinite loop, render => updateState => render => udpateState...
you have multiple options, you can have the get request inside the function called after onClick (not shown in your code), and then update state and pass data as props. In this case you would be making a new get request every single time there's a click event. If you dont need a new get request on every click look into react lifecycle methods, in particular componentDidMount, which is basically executed after the react component is mounted, then you can do a get request there and update the state
componentDidMount: function() {
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
this.setState( { content: log } );
}.bind(this));
},
I can't see from your code what component should be clicked in order to trigger the request, but as far as could see, you should take the request out of the render() method. This method is called every time state/props change, so it might make your code make the request multiple times.
Another thing is that you should always mutate your state by calling this.setState() method, so in your case it would be _this.setState({ content: log }).
Now, if you change the name prop every time another component is clicked, you should do something like:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
componentWillMount: function () {
this.getLog(this.props.source);
},
componentWillReceiveProps: function (nextProps) {
this.getLog(nextProps.source);
},
getLog: function (source) {
var _this = this;
$.get(source, function (data) {
var log = $.parseJSON(data);
_this.setState({
content: log
});
}.bind(this));
}
render: function () {
return (
<div>
{this.state.content[0]}
</div>
);
}
First you can extract the process to create the request call from the render() method into its own method, in this case, getLog(). Then, with two new methods from the React lifecycle, you can call getLog() when the component will be mounted and whenever new props come in from the parents components.

Asyncronous setState not working in componentDidMount, but working elsewhere: React Native

My plan is to get an array of strings from a http request with an authorization token in the header, and then use the array to decide what to render(multiple components means the user is an admin hence he ll be sent to a listView e.t.c)
How I store the authorisation token: (react-native-router-flux is used for navigation)
AsyncStorage.setItem('access_token',token).then((value)=> { Actions.pageTwo()}).done()
//navigates to second page
In the second page have
var HotelAdminService = require('./services.js');
....
constructor(props) {
super(props);
this.state={
keyLoaded:false,
};
this.tester= HotelAdminService.tester.bind(this);
}
componentDidMount() {
HotelAdminService.tester.bind(this);
}
render() {
console.log(this.state);
if (!(this.state.keyLoaded) ) {
return this.renderLoadingView();
}
return (
<View style={styles.viewContainer} >
<SearchBar onChangeText={(e) => this.clickMovie(e)}> </SearchBar>
<ListView dataSource={this.state.dataSource} renderRow= {this.renderMovie} style={styles.listView}/>
</View>
);
}
services.js:
exports.tester=function(){
let REQUEST_URL= config.baseUrl + 'api/HotelAdmin/GetKey';
AsyncStorage.getItem('access_token').then((value) => {
fetch(REQUEST_URL, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + value,
}
}).then((response) => {return response.json()} ).then((responseData) => {
this.setState(
{key: responseData,
keyLoaded: true,
dataSource: this.state.dataSource.cloneWithRows(responseData),
});
}).done();
});
}
Now I know this method works, as it does what I want it to do, however only when I call it from a button click from the second page. What I want to do however is to get the HotelAdmin.tester work at the begining so it can lead to a re-render when I get the data.As you can see My render method gives a loading screen until keyLoaded becomes true. Maybe one alternative would be the tester method to return a string instead of changing the state, however I was unable to return a string array from the nested promise calls.
In its current condition, the code stays stuck in the loading screen as HotelSystemAdmin.tester won't run and change the state
You are only binding the function to a context in componentDidMount. You'll need to invoke it:
HotelAdminService.tester.bind(this)();

Categories

Resources