make for loop wait till data is back from API - javascript

I have a structure like this:
methods: {
function() {
//some stuff in here
for(let i=0; i < array.length; i++) {
//Problem in here or around here
//in here I send something to my API and want to get data back
}
}
}
Explenation what I do: I have an array where I want to loop, for every loop I send data to my API and get data back inside the for-loop.
My problem: I always get the data back in different order because one ot the data is probably bigger and than it takes longer than the others.
Example: I send Array = [1,2,3,4] and want to get it back in the correct order. But 1 needs more time than 2,3,4 and I get than data back in order [2,3,4,1].
How can I assign my for-loop to wait till the first data is back and than go on?
Thank You!

As soon as you've read up on async/await, you'll still wanna run your requests in parallel to save time, rather than sequentially. Promise.all helps here:
async function fetchData () {
const resultsInOrder = await Promise.all(
// pass an array of Promises
array.map(element =>
fetch(/* some arguments based on the element's data */)
.then(res => res.json())
)
)
// do something with results
}

Related

In getServerSideProps, how to execute the return statement after all fetching is complete?

I have access to the username of a person and I can access their corresponding data object using one fetch from firebase. This data object has a lot of properties, including one which is called "uploads" which is an array of documentIDs where each id is that of a post uploaded by the user. I want to fetch all these documents, make it an array and return the ENTIRE array back to the page.
I have written this code so far: https://i.stack.imgur.com/OC74t.png
What is happening is that the return on line 54 executes before all elements of postIDs is looped through because of which, an empty array is returned back to the component.
Let me know if further details are required. Please help me out. Thanks in advance!
Try to change your loop to a for...of:
for (const postID of currentUserData.uploads) {
const postReference = doc(db, `posts/${postID}`)
const snapshot = await getDoc(postReference)
const postData = snapshot.data()
postData.time = postData.time.toJSON()
uploadsArray.push(postData)
}
return { props: { uploadsArray }}
What about try to use Promise.all?
var promises = [];
promises.push();
// Wait for all promises to resolve
Promise.all(promises).then(function(res) {
// Do something...
});

Async/Await variable affectation in javascript for loop

I would like to achieve the following thing in JS but I've got the feeling that I don't have the right approach.
I'm doing a first request giving me a list of objects
For each object I'm doing another request to an API to get matching values
Apply a specific logic to the result
Finally call a function that uses the data provided by the API and modified
My code looks like this :
async function processData(){
const models = await fetch('/api/models').then(response => response.json());
for(let m in models){
for(let i of models[m].items){
if(i.type == 1){
i.values = await fetch('/api/main/'+i.id).then(response => response.json());
}
else{
i.values = await fetch('/api/custom/'+i.id).then(response => response.json());
// + additionnal specific code on values
}
console.log(i.values);//<--- NOT WHAT EXPECTED
}
display(models[m]);
}
}
My problem is that display function is called before i.values is updated.
I think the whole for(let i models[m].items){} loop should be asynchronous and the display function should be called only once all fetch requests for each i item have been resolved.
But I'm struggling to write the code and I'm not sure this is the good way of doing this.

Best way to return full object made from data from different APIs

Im working on an app that mainly is a large form that contains several dropdowns. These dropdowns should
populate with some data from the DB. The app has a server with Express that it works as a proxy: I call it from the front end
and then that server calls some external API's, format the info and then send them back to the front.
Im thinking on having on the Backend a route like "/dropdown" call that route from the front end. And once I get the data (I would like to receive it like this)
data: {
dataForDropdown1:[data],
dataForDropdown2:[data],
dataForDropdown3:[data],
dataForDropdown4:[data],
...etc
}
store it on Redux and call them with a selector and populate all the dropdowns required.
The problem is on the server. I need to get that info from different APIs so in the route in Express I was thinking on use something like
Promise.all([promisefetchUrl1],[promisefetchUrl2],[promisefetchUrl3],...etc)
but I don't know how to format like the object above, because as I understand Promise.all doesn't have an order it just returns the info with the first promise resolved, then the second, etc so it would be hard to know
which info is which.
I was thinking on using async await but its way too dirty, like this:
const info1 = await fetch(url1)
const info2 = await fetch(url2)
const info3 = await fetch(url3)
const info4 = await fetch(url4)
etc
then return it like this
const data = {
dataForDropdown1:info1.data,
dataForDropdown2:info2.data,
dataForDropdown3:info3.data,
dataForDropdown4:info4.data
...etc
}
any advices?
Promise.all doesn't have an order it just returns the info with the first promise resolved, then the second, etc so it would be hard to know which info is which.
It does have an order - the first element in the resolve array will correspond to the first Promise in the array passed to Promise.all, etc, regardless of the order in which the promises resolve.
Use:
const results = await Promise.all([
getData(promisefetchUrl1),
getData(promisefetchUrl2),
getData(promisefetchUrl3),
getData(promisefetchUrl4), // this could be made less repetitive
]);
const data = {
dataForDropdown1: results[0],
dataForDropdown2: results[1],
dataForDropdown3: results[2],
dataForDropdown4: results[3],
};
where getData takes the URL and returns a Promise that resolves to the data corresponding to the URL.

Waiting for loop to finish and then taking the data from it

I'm making discord bot in which I have loop to loop through my JSON data. That works fine however I want to paste that data to Hastebin.
const emails = require("../../json/emails.json");
const hastebin = require("hastebin-gen");
for (var obj in emails) {
if (emails.hasOwnProperty(obj)) {
for (var prop in emails[obj]) {
if (emails[obj].hasOwnProperty(prop)) {
var results = emails[obj]["email"]
}
}
}
var haste = await hastebin(results, { extension: "txt" });
}
console.log(haste)
JSON
{
"test": {
"email": "test#gmail.com"
},
"Test1": {
"email": "mhm#gmail.com"
}
}
As you can see I have 2 emails in JSON so it creates 2 hastebin links and in them is always only 1 email.
EDIT
Didn't really say what is the problem. I want to log only 1 Hastebin url with all the emails inside. Not 2 Hastebin urls and each one has only 1 email.
You need to push all promises to an array and then use Promise.allSettled to wait for all of them to be fulfilled or rejected:
const emails = require("../../json/emails.json");
const hastebin = require("hastebin-gen");
const promises = Object.values(emails).map(({ email }) => hastebin(email, { extension: "txt" }));
const results = await Promise.allSettled(promises);
console.log(results);
According to your comments, if you want to generate a single Hastebin, then you should move the hastebin call after the for loop and pass it all the results concatenated:
const emails = require("../../json/emails.json");
const hastebin = require("hastebin-gen");
const content = Object.values(emails).map(({ email }) => email).join('\n');
const results = await hastebin(content, { extension: "txt" });
console.log(results);
If I understand correctly you want to put all emails into just one file, rather than creating a link for each. The way to correcting this is to understand why it is doing it this way.
Let's first look at where you are creating the link (your hasebin() call), this is inside the for loop. You need to move this outside of your loop, but you want something within your loop still to create an array of the emails you wish to add to hastebin.
Based on your title, you understand this - it sounds like you have a problem with javascripts asynchronous nature, in that your call outside of the loop would not wait for the loop to finish, thus it would not yet have any data within the array.
To achieve this in Javascript we use something called a "callback".
See this example:
function functionOne(callbackFunction) {
console.log("1");
callbackFunction();
}
function onComplete() {
console.log("2");
}
functionOne(onComplete);
The code above would call functionOne with a parameter referencing the onComplete method. functionOne gets ran (1 is printed to console), and then onComplete is called via callbackFunction() and 2 is printed to the console.
So, what does this mean for you example? Your loop should run and then have a callback which is where you create your hastebin, this will ensure your loop has finished constructing/fetching the data prior to trying to upload it.

React-Redux Javascript app - trying to get lots of API calls to fire in order

I'm having an issue with successive API calls (using JQuery's AJAX) to two different APIs in order to build objects with certain attributes. Here's the summary of my app and what I'm trying to do:
The user enters in the name of an actor or director, and the app is meant to return a total of five movies, each of which has certain attributes like title, overview, year, budget, revenue, and a link to a YouTube preview. I'm using The Movie Database API, plus the YouTube API for the YouTube link.
Here's the order of how things currently to work, with all of this happening in the action creator of the Redux app:
Actor name gets sent to the TMDB API -- returns ActorID number
ActorID number gets sent to the TMDB API -- returns 20 movies with: title, overview, year, poster link, and MovieID number
For EACH movie in that list, the MovieID number gets sent to the API -- returns more attributes: budget, revenue, and IMDB-ID (to use in a link later)
Also for EACH movie in step 2, the title gets sent to the YouTube API -- returns a link to the preview.
Once all of this information is assembled for each movie, I want to return the first five movies and dispatch them as the action payload to the Redux store.
I'm using some promises, and I've tried everything I could think of in terms of rearranging the flow of functions, but I can't get all the information I need with one click of the submit button. The funny thing is, it works with TWO clicks of the submit button, I think because by then all the async AJAX calls are finally done. But after the first click, I have an empty array where the movie objects should be.
Here's some code that should summarize what things look like:
var personId
var movies = []
function actorByRating(UserInput) {
Step 1: get actor ID number:
function searchActors() {
return $.ajax({
method: "GET",
url: `https://api.themoviedb.org/3/search/person?query=${UserInput}&api_key=<key>`
}).done(function(response){
personId = response.results[0].id
})
}
Step 2: Use Actor ID to get list of movies, start assigning them attributes:
function getMovies() {
$.ajax({
method: "GET",
url: `https://api.themoviedb.org/3/discover/movie?with_cast=${personId}&vote_count.gte=20&sort_by=vote_average.asc&budget.desc&api_key=<key>&include_image_language=en`
}).done(function(response) {
response.results.forEach((m) => {
var movie = {}
movie.title = m.title
movie.year = m.release_date.split("-")[0]
movie.movieId = m.id
movie.overview = m.overview
movie.poster = "http://image.tmdb.org/t/p/w500" + m.poster_path
getMovieInfo(movie) //step 3
getYouTube(movie) //step 4
saveMovie(movie)
})
})
}
function saveMovie(movie){
movies.push(movie)
}
Step 3 function, takes in a movie object as an argument:
function getMovieInfo(m){
return $.ajax({
method: "GET",
url: `https://api.themoviedb.org/3/movie/${m.movieId}?&api_key=<key>&append_to_results=imdb_id`
}).done(function(response) {
m.revenue = response.revenue
m.budget = response.budget
m.imdbId = response.imdb_id
})
}
Step 4 function, to get Youtube link. Also takes a movie object:
function getYouTube(movie){
$.ajax({
method: "GET",
url: `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${movie.title.split(" ").join("+")}+trailer&key=<key>`
}).done(function(yt){
movie.youtubeLink = `http://www.youtube.com/embed/${yt.items[0].id.videoId}`
})
}
After this, the filtering functions work fine, when they have an array of movies to work with. The problem is, I think, all these successive API calls keep firing before the previous ones are done, and the latter ones need info from the earlier ones to search with. Thus, when I click submit the first time, the final movies array is empty, so the dispatched payload is an empty array. THEN the movie objects get filled in, so when you click submit again, the movies are already there to work with, and the rest of the app works fine.
I've tried everything I can think of to slow the process down, chain promises together (which doesn't work because Step 2 has to run for several movies, i.e. the return values of each function keep changing, so I can't ".then" them), reorganizing the information that comes in...but I can't get it to give me movie objects with all the attributes I need by the time the filtering functions actually run to create the proper payload.
Any help or suggestions would be greatly appreciated!
(Note: the "key" stuff above is just placeholder text)
UPDATE:
I changed the code to basically the following:
searchActors()
.then(function(response){
const actorId = response.data.results[0].id
return actorId
})
.then((personID) => {
return getMoviesFromPersonID(personID)
})
.then(function(response) {
const movieList = []
response.data.results.forEach((m) => {
var movie = {}
movie.title = m.title
movie.year = m.release_date.split("-")[0]
movie.movieId = m.id
movie.overview = m.overview
movie.poster = "http://image.tmdb.org/t/p/w500" + m.poster_path
movieList.push(movie)
// saveMovie(movie)
})
return Promise.all(movieList)
})
.then((movieList) => {
const deepMovieList = []
movieList.forEach((movie) => {
getMovieInfo(movie)
.then(function(response) {
movie.revenue = response.data.revenue
movie.budget = response.data.budget
movie.imdbId = response.data.imdb_id
deepMovieList.push(movie)
})
})
return Promise.all(deepMovieList)
})
.then((deepMovieList) => {
const finalMovies = []
deepMovieList.forEach((movie) => {
finalMovies.push(getYouTube(movie))
})
return Promise.all(finalMovies)
})
Everything works fine right up until the first mention of "deepMovieList". I can't seem to figure out how to have that particular step to work properly, as it essentially involves making 20 API calls with each movie in the movieList. I can't figure out how to 1) get the info back from the API, 2) assign the attributes to the movie object that is passed in to getMovieInfo, and then 3) push that movie object (with the new attributes) to an array that I can use Promise.all on, all without interrupting the promise chain.
Either it moves on to the next "then" function too early (while deepMovieList is still an empty array), or, with other random stuff I've tried, the array ends up being undefined.
How can I have the next "then" function wait until 20 API calls have been made and each movie object has its updated attributes? This will also run into the same problem in the next step, for the YouTube link.
Thanks!
TL;DR: use fetch and promises instead of jQuery, group promises using Promise.all.
The Longer Version
Ok, I'm not going to repeat all your code here. I'm going to abbreviate some stuff to keep it simple.
Basically, you have a bunch of tasks to perform. I'm going to pretend each of them is a function that returns a promise which is resolved with the data you want.
searchActor() - returns a promise resolved with some ID number
getMoviesFromActorID(actorId) - returns a promise that resolves with an array of movie IDs
getMovie(movieId) - returns a promise that resolves with the details for the given movie ID
getYoutube(movie) - returns a promise that resolves with the Youtube embed code.
Given this basic setup (and I admit I'm leaving out a lot of stuff), the code looks like this:
// search for an actor
searchActor('Brad Pitt')
// then get the movie IDs for that actor
.then((actorId) => getMoviesFromActorID(actorId))
// then iterate over the list of movie IDs & build an array of
// promises. Use Promise.all to create a new Promise which is
// resolved when all are resolved
.then((movieIdList) => {
const promiseList = [];
movieIdList.forEach((id) => promiseList.push(getMovie(id)));
return Promise.all(promiseList);
})
// then get Youtube links for each of the movies
.then((movieDetailsList) => {
const youtubeList = [];
movieDetailsList.forEach((movie) => youtubeList.push(getYoutube(movie)));
return Promise.all(youtubeList);
})
// then do something with all the information you've collected
.then((finalResults) => {
// do something interesting...
});
The key to this is Promise.all (documentation here), which will take an array of Promises (or any other iterable object containing promises) and create a new Promise which will resolve when all of the original promises have resolved. By using Promise.all, you can create a step in your promise chain which can include a variable number of parallel actions which must complete before the next step.
You could do something like this will jQuery and callbacks, but it would be pretty darn ugly. One of the great benefits of promises is the ability to lay out a series of steps like above.

Categories

Resources