I'm creating a trivia game using ReactJS, and the following API endpoint: https://opentdb.com/api.php?amount=5&difficulty=medium&type=boolean
From my understanding, I want to consume the API in ComponentDidMount() lifecycle method. From there I'm trying to map() over each item in this response (there should be 5 questions in total), and save them to an empty questions array (which is part of my component's state). Finally, I want to display these questions in a list.
I've tried all sorts of angles including async await, fetch() & .then(), using axios.get, etc. Here's an example of what I've been trying most recently. I'm just not sure how to consume the API, save those questions to the empty questions array to state, and then iterate over, and render the questions in the DOM.
Please note: I have tried console.log(this.state.questions), which shows the response as code 200, and my original API endpoint URL, but no questions data. I don't understand where the questions are at this point! Please help!
class App extends React.Component{
constructor(props){
super(props)
this.state = {
questions: [],
score: 0,
current: 0,
loading: false
}
}
async componentDidMount() {
try {
this.setState({ loading: true })
this.setState({ questions: await fetch('https://opentdb.com/api.php?amount=5&difficulty=medium&type=boolean'), loading: false })
console.log("state", this.state.questions);
console.log("props", this.props.questions);
} catch (err) {
console.log(err)
}
}
}
componentDidUpdate(){
console.log("component just updated!");
}
// another ComponentDidMount Attempt
// async componentDidMount(){
// try {
// this.setState({loading:true})
// this.setState({questions: await fetch('https://opentdb.com/api.php?amount=5&difficulty=medium&type=boolean'), loading: false})
// .then(questions => questions.json())
// .then(questions => this.setState({questions, loading:false}))
// console.log("state", this.state.questions);
// console.log("props", this.props.questions);
// } catch(err) {
// console.log(err)
// }
//
// }
// attempt with axios ()
// componentDidMount() {
// axios
// .get("https://opentdb.com/api.php?amount=5&difficulty=medium&type=boolean")
// .then(response => {
// this.setState({ question: response.data.question });
// this.setState({ category: response.data.category });
// // this.setState({ type: response.data.type });
// // this.setState({ url: response.data.url });
// // this.setState({ score: response.data.score });
// console.log("axios GET worked");
// })
// .catch(err => {
// console.log(
// "Oops, something broke with GET in componentDidMount() - we've got a: ",
// err.message
// );
// });
// }
// below is having an issue with .map() - maybe bc questions
// is object containing arrays(?)
//
// render() {
// return (
// <div>
// {this.state.loading
// ? "loading..."
// : <div>
// {
// this.state.questions.map(question => {
// return(
// <div>
// <h3>Category: {question.category}</h3>
// <h4>Question: {question.question}</h4>
// </div>
// )
// })
// }
// </div>
// }
//
// <HomeCard />
// <QuizCard />
// <ResCard />
// </div>
// );
// }
export default App;
Try with
async componentDidMount() {
this.setState({
loading: true
});
try {
const response = await fetch('https://opentdb.com/api.php?amount=5&difficulty=medium&type=boolean');
const data = await response.json();
this.setState({
questions: data.results,
loading: false
});
} catch (err) {
console.log(err)
}
}
demo at https://codesandbox.io/s/sad-bogdan-g5mub
Your code doesn't work because the call to setState is asynchronous too and because of that your console.log(this.state.question); is executed before of the state update. In order to fix the problem, you can pass a callback as the second argument to setState, this callback will be executed after the state update.
It should look like this:
this.setState(
{
questions: await fetch('https://opentdb.com/api.php amount=5&difficulty=medium&type=boolean'),
loading: false
},
() => {
console.log("questions", this.state.questions);
console.log("loading", this.state.loading);
}
)
You can find more info about here: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value.
I hope that this helps you.
Related
I have a AutoHospitals component and I am trying to get the value of a state variable outside the .then function but it is printing null.
Here is the code snippet where this.state.retrievedmrnNumber is printing.
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
let retrievedMrnNo = response.data.mrnNumber;
this.setState({ retrievedmrnNumber: retrievedMrnNo});
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
Here is the console log statements outside the above .then function, where it is printing null:
console.log("Outside of then function: Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
How do I access it outside of .then function?My ultimate goal is to use the value on this line:
selectedHospitals = [{label: this.props.value[0] && this.state.retrievedmrnNumber || 'Select'}]
Full component code is below:
export class AutoHospitals extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
selectedHospitalValues: null,
selectedHospitals: [],
retrievedmrnNumber:null,
loading: false
};
this.onChange = this.onChange.bind(this);
}
onChange = (val) => {
this.setState({
value: val,
selectedHospitalValues: val
});
this.props.onChange(val)
};
fetchRecords() {
let url = 'myurl'
this.setState({
loading: true
});
return axios
.get(url)
.then(response => {
let selectedHospitals;
if(this.props.value[0]){
console.log('this.props.value is DEFINED - Request has been EDITED!!!!')
// START: Logic to get MRN Number
let hospitalIdtoRetrieveMRNNumber = this.props.value[0].hospitalId;
axios
.get('api/Hospitalses/'+hospitalIdtoRetrieveMRNNumber)
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
let retrievedMrnNo = response.data.mrnNumber;
this.setState({ retrievedmrnNumber: retrievedMrnNo});
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
// END: Logic to get mrn Number
console.log("Outside response block: Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
selectedHospitals = [{label: this.props.value[0] && this.state.retrievedmrnNumber || 'Select'}]
//let selectedHospitals = [{label: this.props.value[0] && 'mrn # 1234' || 'Select'}]
}else {
console.log('this.props.value is UNDEFINED - it is a NEW REQUEST');
}
this.setState({
loading: false
});
if (this.props.value) {
this.props.value.forEach(e => {
selectedHospitals.push(response.data._embedded.Hospitalses.filter(hospitalSet => {
return hospitalSet.hospitalId === e.hospitalId
})[0])
})
}
this.setState({
selectedHospitals: response.data._embedded.Hospitalses.map(item => ({
label: (item.mrnNumber.toString()),
projectTitle: item.projectTitle,
hospitalId: item.hospitalId,
})),
selectedHospitalsValues: selectedHospitals
});
}).catch(err => console.log(err));
}
componentDidMount() {
this.fetchRecords(0)
}
render() {
return (
<div>
<Hospitalselect value={this.state.selectedHospitalsValues} options={this.state.selectedHospitals} onChange={this.onChange } optionHeight={60} />
<div className="sweet-loading" style={{ marginTop: '-35px' }}>
<ClockLoader
css={override}
size={30}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
It's all about sync\async. Consider following two examples:
With then which is fully async (and do not allowing any waits) :
export const download = (url, filename) => {
fetch(url, {
mode: 'no-cors'
/*
* ALTERNATIVE MODE {
mode: 'cors'
}
*
*/
}).then((transfer) => {
return transfer.blob(); // RETURN DATA TRANSFERED AS BLOB
}).then((bytes) => {
let elm = document.createElement('a'); // CREATE A LINK ELEMENT IN DOM
elm.href = URL.createObjectURL(bytes); // SET LINK ELEMENTS CONTENTS
elm.setAttribute('download', filename); // SET ELEMENT CREATED 'ATTRIBUTE' TO DOWNLOAD, FILENAME PARAM AUTOMATICALLY
elm.click(); // TRIGGER ELEMENT TO DOWNLOAD
elm.remove();
}).catch((error) => {
console.log(error); // OUTPUT ERRORS, SUCH AS CORS WHEN TESTING NON LOCALLY
})
}
With await, where the response becomes sync:
export const download = async (url, filename) => {
let response = await fetch(url, {
mode: 'no-cors'
/*
* ALTERNATIVE MODE {
mode: 'cors'
}
*
*/
});
try {
let data = await response.blob();
let elm = document.createElement('a'); // CREATE A LINK ELEMENT IN DOM
elm.href = URL.createObjectURL(data); // SET LINK ELEMENTS CONTENTS
elm.setAttribute('download', filename); // SET ELEMENT CREATED 'ATTRIBUTE' TO DOWNLOAD, FILENAME PARAM AUTOMATICALLY
elm.click(); // TRIGGER ELEMENT TO DOWNLOAD
elm.remove();
}
catch(err) {
console.log(err);
}
}
The await example can be called as anonymous function (hope the normal call also possible):
(async () => {
await download('/api/hrreportbyhours',"Report "+getDDMMYYY(new Date())+".xlsx");
await setBtnLoad1(false);
})();
I believe the Promise from the async axios.get function hasn't resolved by the time you call the state value in selectedHospitals
Try passing a callback function to the return of the then statement:
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
handleRequest(response.data.mrnNumber);
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
And here is the callback which can use setState:
handleRequest(data) {
this.setState({ retrievedmrnNumber: data});
}
EDIT To bind handle request to this properly try making it an arrow function:
handleRequest = (data) => this.setState({retrievedmrnNumber:data});
I am learning about how to use synchronous setState but it is not working for my project. I want to update the state after I get the listingInfo from Axios but it does not work, the res.data, however, is working fine
class ListingItem extends Component {
constructor(props) {
super(props);
this.state = {
listingInfo: {},
open: false,
};
this.getListingData(this.props.itemId);
}
setStateSynchronous(stateUpdate) {
return new Promise((resolve) => {
this.setState(stateUpdate, () => resolve());
});
}
getListingData = async (item_id) => {
try {
const res = await axios.get(`http://localhost:5000/api/items/${item_id}`);
console.log(res.data);//it's working
await this.setStateSynchronous({ listingInfo: res.data });
// this.setState({
// listingInfo: res.data,
// });
console.log(this.state.listingInfo);//no result
} catch (err) {
setAlert('Fail to obtain listings', 'error');
}
};
I would be really grateful for your help!
Thanks to #PrathapReddy! I used conditional rendering to prevent the data from rendering before the setState is done. I added this line of code on the rendering part:
render() {
if (Object.keys(this.state.listingInfo).length === 0) {
return (
<div>
Loading
</div>
);
} else {
return //put what you want to initially render here
}
}
Also, there is no need to modify the setState, the normal setState will do. Hope this is useful!
I am trying to call getSession() every 5sec of delay. But in initial render i would like to call this function and execute immediately.
According to my below code, in the initial render itself it is using the delay of 5sec to display the output.
How can i achieve the following:
1. Initial render should be done immediately
2. after every 5sec getSession() should be called as well.
Current Results:
It is taking 5sec delay to display in initial render.
Expected results:
Initial render should be done immediately.
componentDidMount() {
this.getSession();
}
getSession() {
var path = "Sharing.aspx/GetSessions";
setInterval(() => {
axios
.post(path, { withCredentials: true })
.then(response => {
let element = response.data.d;
this.setState({
sessions: element
});
})
.catch(error => {
this.setState({
Errors: error
});
console.error(error);
});
},5000
);
}
render() {
return (
<div>
{this.renderSessionDetails()}
</div>
);
}
Expected results:
Initial render should be done immediately.
After every 5sec getSessions() should be called.
I would do something like this:
const INTERVAL = 6000;
class Component extends React.Component {
componentDidMount() {
this.getSession();
this.intervalId = window.setInterval(() => this.getSession(), INTERVAL);
}
componentWillUnmount() {
window.clearInterval(this.intervalId);
}
getSession() {
var path = "Sharing.aspx/GetSessions";
setInterval(() => {
axios
.post(path, { withCredentials: true })
.then(response => {
let element = response.data.d;
this.setState({
sessions: element
});
})
.catch(error => {
this.setState({
Errors: error
});
console.error(error);
});
}, 5000);
}
render() {
return <div>{this.renderSessionDetails()}</div>;
}
}
ComponentDidMount will be called only once, and at that point, you call the first getSession call, and start the interval.
An important thing to bring attention to is the call to window.clearInterval when the component gets unmounted. This is to make sure that interval doesn't keep running eternally, and worst, that more than one interval run in parallel after having this component mount a couple of times.
I hope it helps.
You could go about refactoring your code to look like that, in order to avoid waiting initially for those 5 seconds. The refactor is mainly about extracting the fetching logic away from the timer implementation. Please note that inside componentDidMount() we first call this.getSession() immediately, which is fine because we eliminated the intervals from it. Then we dispatch the intervals.
class Component extends React.Component() {
intervalId = null
componentDidMount() {
this.getSession()
this.intervalId = setInterval(() => this.getSession(), 5000)
}
componentWillUnmount() {
if (this.intervalId) {
clearInterval(this.intervalId)
}
}
getSession() {
var path = 'Sharing.aspx/GetSessions'
axios
.post(path, { withCredentials: true })
.then(response => {
let element = response.data.d
this.setState({
sessions: element
})
})
.catch(error => {
this.setState({
Errors: error
})
console.error(error)
})
}
render() {
return <div>{this.renderSessionDetails()}</div>
}
}
I would also try to make sure we're not running into race conditions here. But, if you're sure your requests never take more than 5 seconds -- it should be fine. Hope it helps!
In my app component I am fetching couple things so there's couple actions. It's a state component. When one of the actions ends isLoading property changes to false and screen loading disappears. But it doesn't work properly because one action can take longer than another. How Can I change my isLoading property to false after all async actions are done?
My code looks something like
componentDidMount() {
this.props.fetchA();
this.props.fetchB();
this.props.fetchC().then(() => {
this.setState({isLoading: false})
})
}
You can chain those promises like this
componentDidMount() {
this.setState({ isLoading: true}); // start your loader
this.props.fetchA()
.then(() => {
return this.props.fetchB();
})
.then(() => {
return this.props.fetchC()
})
.then(() => {
this.setState({ isLoading: false }); // Once done, set loader to false
})
.catch(error => {
console.log('Oh no, something went wrong', error);
});
}
or using async/await with try catch do something fancy like this.
constructor () {
super();
this.state = {
isLoading: false,
};
this.onLoadData = this.onLoadData.bind(this); // see what I did here, i binded it with "this"
}
componentDidMount() {
this.onLoadData(); // Call you async method here
}
async onLoadData () {
this.setState({ isLoading: true}); // start your loader
try {
const awaitA = await this.props.fetchA();
const awaitB = await this.props.fetchB();
const awaitC = await this.props.fetchC();
this.setState({ isLoading: false }); // Once done, set loader to false
} catch (e) {
console.log('Oh no, something went wrong', error);
}
}
I have the following code and getting the values through the api and set to the state variable but the view is rendered before setting the value to the state. So i could not display the value in my view. How could i change the code to work fine?
this.state = {
activeJobs: [],
isLoading: true
};
componentDidMount(){
axios.get(this.state.url+'/tables')
.then(response => {
// If request is good...
const isLoading = true,
activeJobs = response.data.activeJobs;
this.setState({ activeJobs });
})
.catch((error) => {
console.log('error ' + error);
});
}
render() {
console.log(this.state.activeJobs)
<p className="text">{!this.state.isLoading && this.state.activeJobs.count} Jobs</p>
}
The console i have given inside the render shows blank array. I also tried by changing the function componentDidMount() to componentWillMount() but getting the same result.
There is no way to ensure that an async request will complete before rendering. You can display proper messages in render to reflect the status of the request.
For example - before calling axios, set the state to 'in process' or 'loading', so that render will show an appropriate message. Then, when loading finished successfully or with an error, set the state appropriately to render an appropriate message in the error case, and the result otherwise.
If you can't render yet, then simply return null:
render() {
if (!this.state.activeJobs && !this.state.isLoading) {
return null;
}
return (
<div>
{ this.state.isLoading && <p className="text">Loading...</p> }
{ !this.state.isLoading && <p className="test">{ this.state.activeJobs.count } Jobs</p>
</div>
);
}
In order to set isLoading, set it before the HTTP call:
componentDidMount(){
this.setState({ isLoading: true });
axios.get(this.state.url+'/tables')
.then(response => {
// If request is good...
const activeJobs = response.data.activeJobs;
this.setState({ activeJobs, isLoading: false });
})
.catch((error) => {
console.log('error ' + error);
});
}