I have an arrow function that is invoked from cDM to retrieve the updated status of schedules every 20 seconds with a setTimeout(). The setTimeout() method never fires another request to the server like it's supposed to. So I'm thinking the code is never reached. I'm not sure exactly how to modify it so the method is reached.
componentDidMount() {
//get request to /schedules
//update state with response data
this.getUpdatedStatus();
}
getUpdatedStatus = () => {
//fetch updated status,
//list of arrays that are updated to state in `cDM`
const schedules = this.state.schedules;
Promise.all(
schedules
.map(schedule =>
axios({
method: "get",
url: schedule.selfUri,
headers: {
Accept: " someValue"
}
})
)
.then(response => {
if (response.data.data[0].status !== "Complete") {
this.timeout = setTimeout(() => this.getUpdatedStatus(), 20000);
}
console.log(response);
this.setState(
{
scheduleStatus: response.data.data[0].status,
},
() => {
console.log(this.state.scheduleStatus);
}
);
})
).catch(error => console.log(error.response));
};
Codesandbox
in simple words, you are using wrong the Promise.all(), this is because the structure is something like:
Promise.all([p1, p2,...,pn]).then([r1, r2, ..., rn])
but your code is something like:
Promise.all([p1, p2,...,pn].then(r))
so basically your promise.all should be changed to something like this:
getUpdatedStatus = () => {
//fetch updated status,
//list of arrays that are updated to state in `cDM`
const schedules = this.state.schedules;
Promise.all(
schedules
.map(schedule =>
axios({
method: "get",
url: schedule.selfUri,
headers: {
Accept: " someValue"
}
})
))
.then(responses => {
//now you have to iterate over the responses
const isIncomplete = responses.some(r => r.data.data[0].status !== "Complete")
if (isIncomplete) {
this.timeout = setTimeout(() => this.getUpdatedStatus(), 20000);
}
console.log(responses);
this.setState(
{
scheduleStatus: isIncomplete?'Pending':'Complete',//improve this piece of code
},
() => {
console.log(this.state.scheduleStatus);
}
);
})
};
here you have a working sandbox with the code that you provided on your sandbox.
Well, basically #PrinceHernandez found the main issue, but there were a lot of other problems with your code:
input field was read only
missing tbody
missing key attribute on each tr
I took the liberty to polish as I could, here: https://codesandbox.io/s/6vyk48x18n
Open console and you will see that the code updates every 2s printing 1. Moving thenable to each schedule made necessary to have a flag controlling ONE invocation of timeout per getUpdatedStatus.
Related
I have this function in my React App. It calls a few other functions. Everything works except, I need the .then(() => this.getHCAid()) to complete before .then(() => this.addDocsList()) runs. I have not been able to make that happens. I'm sure it's simple, I just don't know how.
createHCA() {
fetch(API_URL + `/hca/create`, {
method: "PUT",
body: JSON.stringify({
client: this.state.client,
short: this.state.short,
}),
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (!res.ok) {
throw new Error();
}
return res.json();
})
.then((data) => console.log(data))
.catch((err) => console.log(err))
.then(() => this.getHCAid()) <--- Need this to complete
.then(() => this.addDocsList()) <--- Before this runs
.then(() => this.getAllHCAs());
this.setState({ showHide: false});
}
I'd encourage you to do a couple of things here...
Use async/await rather than a myraid of .then promises memoize
these function calls and use a useEffect (or multiple) with
dependencies to control when side-effects should be triggered. Convert to functional component to achieve this result.
"showHide" is a very confusing term for future developers, be kind to
your future self and future devs and make a term like "show" or
"isHidden"
If you really want to continue this pattern (chained .then statements), put your .catch at the very bottom to handle any issues/errors. This is where async/await is worth the time to quickly learn.
You only want functions doing one main thing, and this is to createHCA, let your other code trigger the functions. As you scale you will be thankful you did.
async function createHCA() {
let data
try {
data = await fetch(API_URL + `/hca/create`, {
method: "PUT",
body: JSON.stringify({
client: this.state.client,
short: this.state.short,
}),
headers: { "Content-Type": "application/json" },
})
} catch e => {
throw new Error(e.message)
return
}
this.setState({ showHide: false});
return data?.json()
}
It sounds like there are two issues going on here.
The first is that your functions sound like database queries or updates which will be asynchronous so you can't rely on the next step in the cycle to have access to any data returned if it hasn't been told to wait.
The solution you tried to come up with, setting state in one of the then methods, won't work either because it too batches up queries and processes them asynchronously, and the next step won't have access to that data either.
So, ideally what you should probably do is use a promise to return the id from getHCAid, and then pass that into addDocsList which also returns a promise, etc.
Here's a simplified example using async/await.
getHCAid() {
return new Promise((res, rej) => {
// Do DB process, get an id
res(id);
});
}
async getData() {
const response = await fetch(API_URL);
const data = await response.json();
const id = await this.getHCAid();
const list = await this.addDocsList(id);
const hcas = await this.getAllHCAs();
this.setState({ showHide: false });
}
Good day all,
I'm working on extracting some data out of PipeDrive's API using Axios for Node.js. The way that PipeDrive developed their API pagination is a bit different. Here is how their pagination indicator looks:
"additional_data": {
"pagination": {
"start": 0,
"limit": 100,
"more_items_in_collection": true,
"next_start": 100
}
}
I need to interate through all pages to extract the data, and my code has successfully done that, but I cannot get my promise to resolve for some reason.
My logic in the code is as follows:
if(more_items_in_collection){Add current page's data then re-run the same function with the next_start as a parameter}
else{Add current page's data then RESOLVE the promise to complete the function}
But this resolution never happens, even though my code works (strange).
Gurus, can you please take a look at my code and let me know why you think it won't resolve (function().then((result) => {OUTPUT}) never returns happens)?
Thanks as always!
const queryPipeDrive = (start) =>{
// Query is a full then-able Async function
return new Promise((resolve, reject) => {
// API CALL
axios({
method: 'GET',
url: pipeDriveURI,
params: {
api_token: apiKey,
start: start
}
})
// THEN DO THIS
.then((response) => {
// IF there are more items in collection on additional pages, iterate through the pages
if(response.data.additional_data.pagination.more_items_in_collection){
// Add all deals on page to the container
for (const deal of response.data.data) {
db.get('deals').push(deal) // Push deal data to the StormDB File
}
console.log(chalk.cyan(`${response.data.additional_data.pagination.next_start}`))
// Function loop created. We will loop UNTIL the 'more_items_in_collection' prop is false, then we'll resolve the promise.
queryPipeDrive(response.data.additional_data.pagination.next_start)
}else{
// Add all deals on this page to the reponse container
for (const deal of response.data.data) {
db.get('deals').push(deal)
}
db.save() // Save changes to temp DB
resolve(response.data.data) // Resolve Promise with the data from the successful call
}
})
.catch((err) => {
console.log(chalk.red(err))
reject(err)
})
})
}
Your more_items_in_collection case never resolves the promise. It just creates a new one, then does nothing with it.
Additionally, you're making your code more complicated than it needs to be by using new Promise. Axios already returns a promise, so there's no need to explicitly create a new one. Calling .then will create a new promise automatically, which resolves to whatever value you return in the callback.
const queryPipeDrive = (start) => {
// API CALL
return axios({
method: "GET",
url: pipeDriveURI,
params: {
api_token: apiKey,
start: start,
},
})
// THEN DO THIS
.then((response) => {
// IF there are more items in collection on additional pages, iterate through the pages
if (response.data.additional_data.pagination.more_items_in_collection) {
// Add all deals on page to the container
for (const deal of response.data.data) {
db.get("deals").push(deal); // Push deal data to the StormDB File
}
console.log(
chalk.cyan(`${response.data.additional_data.pagination.next_start}`)
);
// Function loop created. We will loop UNTIL the 'more_items_in_collection' prop is false, then we'll resolve the promise.
return queryPipeDrive(
response.data.additional_data.pagination.next_start
);
} else {
// Add all deals on this page to the reponse container
for (const deal of response.data.data) {
db.get("deals").push(deal);
}
db.save(); // Save changes to temp DB
return response.data.data;
}
})
.catch((err) => {
console.log(chalk.red(err));
throw err;
});
};
Besides the accepted answer.
Would you consider using this async function in await? In this way, you call the main().
const main = async start => {
const res = await queryPipeDrive(start);
if (res.isMoreItems === true) {
await main(res.nextStart);
}
};
async function queryPipeDrive(start) {
const response = await axios({
method: "GET",
url: pipeDriveURI,
params: {
api_token: apiKey,
start: start,
},
});
for (const deal of response.data.data) {
db.get("deals").push(deal);
}
if (response.data.additional_data.pagination.more_items_in_collection) {
console.log(
chalk.cyan(`${response.data.additional_data.pagination.next_start}`)
);
return {
isMoreItems: true,
nextStart: response.data.additional_data.pagination.next_start,
};
} else {
db.save(); // Save changes to temp DB
return {
isMoreItems: false,
};
}
}
I have an array of table names, so I need to fetch response of three tables. See the below code. Once the data is appended to the dom I need to call the successMessage method, now I am using setTimeout how can I use promise in the this scenario
let lists = ['table1', 'table2', 'table3']
lists.map(list => {
$.ajax({
url:`${rootUrl}/api/('${list}')`,
type: 'GET',
headers: {
accept: 'application/json'
},
success: res => dataDisplay(res),
error: err => console.log(JSON.stringify(err))
})
})
// displaying data
const dataDisplay = (res) => {
switch(res.TableName){
case 'Table1':
$("#tbl1 p").text(res.TableOriginalName)
$("#tbl1 .content p").text(res.TableDescription)
break;
case 'Table2':
$("#tbl2 p").text(res.TableOriginalName)
$("#tbl2 .content p").text(res.TableDescription)
break;
case 'Table3':
$("#tbl3 p").text(res.TableOriginalName)
$("#tbl3 .content p").text(res.TableDescription)
break;
default:
return
}
}
// successfully data appended
const successMessage = () => alert("data appended successfully")
// calling the success method once data is appended
setTimeout(successMessage, 3000)
You'd use Promise.all to wait for all of those requests to finish before showing the message. First, build up an array of the promises:
var promises = lists.map(list => $.ajax({
url:`${rootUrl}/api/('${list}')`,
type: 'GET',
headers: {
accept: 'application/json'
},
success: res => dataDisplay(res),
error: err => console.log(JSON.stringify(err))
}));
then wait for them to complete
Promise.all(promises).then(() => alert("data appended successfully"));
You can also use $.when for much the same purpose, but it's awkward to call:
$.when.apply($, promises).done(() => ...);
In comments you've said that dataDisplay loads a bunch of images and you need to delay the call to successMessage until after those images have loaded. To do that, you'll need to watch for the load event on the images. This can be a bit squirrelly because the images can load before you hook the event, so we'll want to use the image's complete flag as well. Something along these lines:
Promises.all(/*...*/).then(() => {
// Get all images in the tables we added to
let imgs = $("#tbl1 img, #tbl2 img, #tbl3 img");
// Hook up a function to check for completed images when we
// see a `load` event on any of them, and fire that proactively
imgs.on("load", checkComplete);
checkComplete();
function checkComplete() {
// Count any incomplete images; remove the handler from any
// complete image and remove it from our `imgs` set
let incomplete = 0;
imgs.get().forEach(img => {
if (img.complete || img.error) {
$(img).off("load", checkComplete);
imgs = imgs.not(img);
} else {
++incomplete;
}
});
if (incomplete == 0) {
// They're all done!
successMessage();
}
}
});
That's off the top of my head, may need some tweaking, but it should get you headed the right way.
You may try this code:
let tableNames = ['table1', 'table2', 'table3']
let promiseArr = tableNames.map((table) => {
return new Promise((resolve, reject) => {
$.ajax({
url:`${rootUrl}/api/('${list}')`,
type: 'GET',
headers: {
accept: 'application/json'
},
success: (res) => {
dataDisplay(res);
resolve(table);
},
error: (err) => {
console.log(JSON.stringify(err));
reject(table);
}
});
}).catch((e) => {
// if call to any of the table's url fails
// it will come here, var 'e' contains
// that table name, handle here
console.log(e + " has fails"); return e;
});
});
Promise.all(promiseArr).
then((result) => {
// this will run regardless of, if call to any table fails
console.log("success")
})
.catch((result) => {
console.log(result + " fails");
});
this will asynchronously call the tables, and at the end comes to Promise.all()'s then() even if call to some table fails
I make HTTP request with axios and inside it I make another HTTP request in order to add some details about items that I get. I need to setState after I push it to the 'orders' Array, but it does it before so I can't print it in the correct way.
It works with SetTimeout but I want to do it in more professional way.
How can I do it synchronous??
fetchOrders(){
let orders = [];
let status;
this.setState({loading:true});
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
})
.catch(error => {
console.log(error);
})
});
setTimeout( () => {
this.setState({orders, error: status ? false : true, loading:false})
}, 1000);
})
.catch(error => {
this.setState({loading:false, error:true});
console.error(error);
});
}
You seem to be asking the wrong question, which is how to implement something async as sync, instead you really are trying to defer setting your state until everything is finished. (aka the XY Problem)
You don't need to make setState synchronous - you just need to leverage some promise chaining and Promise.all, which will allow you to defer the setState call until everything is finished.
In short you should be able to adapt this to something you need, applying a few more transformations to the responses:
fetchOrders() {
http.get('/first')
.then(r => r.data)
.then(orders => {
// This wraps the iterable of promises into another promise
// that doesn't resolve until all the promises in the iterable
// have finished.
return Promise.all(orders.map((order) => http.get(`/second/${order.id}`).then(/* transform */))
})
.then(transformedOrderPromises => this.setState({ ... });
}
setState can take a callback function, after finishing mutate the state it will execute the callback. So you can setState and add your 2nd API call in the callback.
Something like this:
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
this.setState({orders, error: status ? false : true, loading:false},
() => {
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
}).catch(error => {
console.log(error);
})
});
})
Please note that I just edited a dirty way, may be you need to make some adjustment to make it works.
In my projects, I fake a synchronous setState to avoid lot of pitfalls, making the code cleaner. Here's what I do:
class MyComponent extends React.Component {
// override react's setState
setState(partialState) {
const mergedState = { ...this.state, ...partialState };
this.state = mergedState;
super.setState(partialState);
}
}
Explained: before calling the true setState, the state is also set into this.state so that any reference to it before the actual update is correct.
The only downside is that you have to extend from MyComponent rather than React.Component.
I have a code where for each of the ids I am making an ajax request and processing them as results come in. Here is a simple replica of my actual code and jsfiddle:
var ids = [1,2,3,4,5,6];
var ids$ = (id) => {
return Observable.of(id);
};
var loadIds$ = (id) => {
if(id == 4) return xhr('/echo/jsoneee/', {id: id});
return xhr('/echo/json/', {id: id});
};
Observable.from(ids)
.concatMap(id => Observable.forkJoin(ids$(id), loadIds$(id)))
.catch((err, caught) => {
//get the id for which loadIds failed
//send the id to subscribe() and continue the subscription
return Observable.empty();
})
.subscribe(result => console.log(result))
But now I need to modify the code so that if an error occurs I will have to get the id for which the ajax request failed and then just continue the subscription like nothing happened. I have not been able to do this yet. Help is really appreciated.
I think you can simplify this significantly by emitting correct values right in Observable.create(...):
function xhr(url, json) {
return Observable.create(function (observer) {
$.ajax({
url: url,
data: JSON.stringify(json),
success: function (response) {
observer.next([json.id, json]);
},
error: function (jqXHR, status, error) {
observer.next([json.id]); // <== Notice this
},
complete: function () {
observer.complete();
}
});
});
}
var ids = [1,2,3,4,5,6];
var ids$ = (id) => {
return Observable.of(id);
};
var loadIds$ = (id) => {
if(id == 4) return xhr('/echo/jsoneee/', {id: id});
return xhr('/echo/json/', {id: id});
};
Observable.from(ids)
.concatMap(id => loadIds$(id))
.subscribe(result => console.log(result));
This way you can avoid forkJoin() completely. Also be aware that catch() operator automatically unsubscribes from its source. This operator is intended to continue with another Observable so it's now very useful in cases such as yours.
You could of course use:
.catch((error, caught) => {
return caught;
})
This however causes resubscription and thus reemission of all values from the beginning which is usually undesired.
There's also onErrorResumeNext() operator that simply ignores the errors but that's probably not what you want.
See demo: https://jsfiddle.net/4f1zmeyd/1/
A slightly similar question: get new ticket then retry first request