Recently I had some issues using built-in map() function on array in JavaScript, which I managed to solve by using standard for loop. I've look through posts on StackOverflow to find out where is the difference between those two methods (and forEach() which I tested just for sake of it). I understand that map() is creating a new array by executing the function provided, whereas forEach() (and I believe for as well) are not creating a new array.
Could anyone please explain where is the difference in how the functions are executed?
Tested scenario: Frontend in ReactJS receives data from the backend over http request. The received data contains array of objects, where one property is image in base64 string. I defined the function which converted each image to image object (using Jimp), inverted the color, change to base64 and saved back in the array as JSX object to be displayed on the page. Three version of code looked as followed:
FOR LOOP:
console.log("before for");
for(n of result) {
console.log("inside for ", counter);
await this.processImage(n, counter++);
}
console.log("end for");
this.setState(() => {
console.log("Rendered");
return {
rows: result
}
})
FOREACH():
console.log("before foreach");
result.forEach(async (n) => {
console.log("inside foreach ", counter);
await this.processImage(n, counter++);
counter++;
})
console.log("end foreach");
this.setState(() => {
console.log("Rendered");
return {
rows: result
}
})
MAP():
console.log("before map");
result.map(async (n) => {
console.log("inside map ", counter);
await this.processImage(n, counter++);
counter++;
})
console.log("end map");
this.setState(() => {
console.log("Rendered");
return {
rows: result
}
})
I included the console.logs in the code above to show how I was testing the execution of the code. In each scenario what I got was (before, inside x3, end, rendered) in the same order. Unfortunately, map() and forEach() didn't perform the whole image processing and what I could see on the page instead of an image was super-long string. The for loop didn't fail a single time.
I understand in this situation I probably don't need to use map() as I don't have to create a new array. I would still like to know why the result was not always guaranteed, so I can avoid issues in the future.
I want to properly understand how those functions work, so I can use them correctly in the future. The documentation I read on is not very clear to me, I hope you guys can help!
Thanks
By using an async function inside .map and .forEach you fire and forget the asynchronous action, which means that you won't know when it finished. The async function does however return a Promise, and if you use .map you could collect them in an array, call Promise.all on that and await that:
await Promise.all(result.map(async (n) => {
console.log("inside map ", counter);
await this.processImage(n, counter++);
counter++; // why increase counter twice?
}));
// all processings are guaranteed to be done
This will execute all the processing in parallel, which is probably (way) faster than processing sequentially which you'd do with the for loop. Using a .forEach you aren't able to wait for all results to arrive, and therefore you probably don't want to use it in most cases.
If you arent doing asynchronous things, for and .forEach would behave nearly equal, except for arrays with empty slots (.forEach skips empty slots):
for(const el of Array(3))
console.log(el); // logs undefined three times
Array(3).forEach(console.log) // silence
.map behaves like .forEach just that it builds up an array with the returned values, just as you said.
Related
I am trying to loop through an array and for each element in an array i want to use getDoc() and then add the data of the document to a new array. Basically to do this:
useEffect(() => {
let items = [];
for(const cart_item of cartItems){
getDoc(doc(getFirestore(), 'tickets', cart_item.ticket_id)).then((ticket_item) => {
items.push({...ticket_item.data()});
});
}
console.log(items);
}, [cartItems])
However, the items array is still empty after a loop where i log it. I think that the issue is that the loop is not waiting for the getDoc() to return something and goes on. How to make it wait for the getDoc() to finish? I have been reading about async, await but i still don't understand how to handle it.
Try refactoring your code with an async function as shown below:
useEffect(() => {
const getItems = async () => {
// fetching all documents by mapping an array of promises and using Promise.all()
const itemsDocs = await Promise.all(cartItems.map(c => getDoc(doc(getFirestore(), 'tickets', c.ticket_id)))
// mapping array of document data
const items = itemsDocs.map(i => i.data())
console.log(items);
}
getItems();
}, [cartItems])
When you call getDoc it needs to make a call to the Firestore server to get your data, which may take some time. That's why it doesn't immediately return the document snapshot, but instead you use a then() callback that the Firestore SDK calls when it gets the data back from the server.
This sort of asynchronous call changes the order in which you code executes, which is easiest to see if we add some logging to the code:
console.log("Before loading data");
for(const cart_item of cartItems){
getDoc(doc(getFirestore(), 'tickets', cart_item.ticket_id)).then((ticket_item) => {
console.log("Got data");
});
}
console.log("After starting data load");
Then you run this code, the logging output i:
Before loading data
After starting data load
Got data
Got data
...
This is probably not what you initially expected, but it perfectly explains why your console.log(items) shows an empty array: by that time none of the calls to items.push({...ticket_item.data()}) have run yet, because the data is still being loaded from the server.
I noticed that Dharmaraj just posted an answer with the code for a working solution using Promise.all, so I recommend checking that out too.
I've been encountering an issue regarding JS promise use, and hopefully it is simply that I am missing something very obvious.
Essentially, I attempt to read multiple JSON files at once and push their content to an array belonging to another object, then perform operations on the elements on this array. Therefore, the array needs to be filled before operations are attempted on them. However, despite me using promises to theoretically make sure the order is correct, it seems what I've written fails at doing that.
How do I fix this issue?
Here are snippets of the code I'm using, where the issue arises:
This is the function where I push the extracted objects to my array:
function pushNewRoom (ship, name_json_folder, elem, id) {
let promiseRoom = new Promise ((resolve, reject) => {
let newRoom = gf.getJSONFile(name_json_folder + '/' + elem + ".json")
// note: getJSONFile allows me to grab a JSON object from a file
.then(
(data) => {
data.name = elem;
ship.rooms.push(data);
return data;
}).then((newRoom) => {
resolve(newRoom);
}).catch((reject) => { // if the JSON file doesn't exist a default object is generated
let newRoom = new Room (elem, id);
ship.rooms.push(newRoom);
resolve(newRoom);
});
});
return promiseRoom;
}
And this is the part that calls that function and performs the operations I need after that:
exports.generateRoomsByLayout = function (name_json_folder, ship)
{
ship.rooms = [];
console.log("reached step 1");
// First execution step: get a JSON file
gf.getJSONFile(name_json_folder + "/_base_layout.json")
.then(function (shipmode){
// Note: shipmode is a JSON object that acts as a blueprint for the operations to follow.
// Importantly here, it contains an array, layout, containing the names of every other JSON file I will need to perform the operations.
console.log("reached step 2");
Promise.allSettled(shipmode.layout.map(function (elem, index){pushNewRoom(ship, name_json_folder, elem, index);})
// note: this is where my issue happens
).then(function (){
console.log("reached step 3");
// Operations on the results that were pushed to ship.rooms by pushNewRoom()
}).then(function (shipmode) {
console.log("reached step 4");
// More operations on the results
}).catch((err) => {
});
return Promise.resolve(shipmode);
}).catch(function (errRejection) {
// Error handling
console.log(errRejection);
});
};
The issue happens right at the Promise.allSettled() line. Rather than waiting for the promises supposedly generated with ship.layout.map(), that would then become an iterable array, the program continues on.
I suppose this is because Promise.allSettled() does not wait for the array to be generated by map() before moving on, but have been unable to fix the issue, and still doubt that this is the explaination. Could anyone enlighten me on what I am doing wrong here?
If what I'm asking is unclear then please do tell me, and I'll try my best to clarify.
edit: I suspect it is linked to Promise.allSettled() not waiting until map() fills the array to consider every promise inside the array settled, as its length seems to be 0 right at step 3, but I am not sure.
Nevermind, I'm an idiot.
The map() method's callback (?) won't (obviously) return an object (and therefore, a promise) if you do not tell it to. Therefore,
shipmode.layout.map(function (elem, index){pushNewRoom(ship, name_json_folder, elem, index);})
needs to be
shipmode.layout.map(function (elem, index){return pushNewRoom(ship, name_json_folder, elem, index);})
I have a problem in assuring the synchornous execution of an array which is executed within another array. The first array is NOT directly link to the "Nested" one - it just assures the the second ("nested") array is executed as many times, as the first one has recrods resp. documents.
To illustrate the Problem - here is the code I am talking about
Promise.all(
room.connections.map(connection => {
Question.find({room: room.title}).then(questions => {
return Promise.all(
questions.map(question => {
if (question.answers.length !== 2) {
question.answers.push({ email: connection.userId, own: "", guess: "" });
console.log('SAVE ANSWER');
return question.save();
}
}),
)
});
})
).then(() => {
console.log('SENDING GAME READY TO BOTH!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
io.of("/game")
.in(room.title)
.emit("GameReady", true);
ack(false);
})
So as you see - I am inserting answers into a question array. Besides that there should be inserted as many answers, as I have active connections in another Collection.
I have tried the code above, but obviously the first promise.all resolves, as soon as the two connections where iterated through - without assuring the all answers have already been inserted/saved.
I have initially tried to make the whole thing without the first Promise.all - but had the problem that the "socket.emit" part would then be executed twice (because I have 2 connections in my array usually).
The outermost lambda (connection => { ... }) has a statement body (because the body is surrounded by curly braces), but doesn’t contain a return statement, so the expression room.connections.map(...) is evaluating to a collection full of undefined values. And something like Promise.all([ undefined ]) will immediately resolve.
Try returning Question.find(...) from the outer lambda. That way, the outermost call to Promise.all will receive as argument a collection that’s properly populated with promises.
Synchronicity in js loops is still driving me up the wall.
What I want to do is fairly simple
async doAllTheThings(data, array) {
await array.forEach(entry => {
let val = //some algorithm using entry keys
let subVal = someFunc(/*more entry keys*/)
data[entry.Namekey] = `${val}/${subVal}`;
});
return data; //after data is modified
}
But I can't tell if that's actually safe or not. I simply don't like the simple loop pattern
for (i=0; i<arrayLength; i++) {
//do things
if (i === arrayLength-1) {
return
}
}
I wanted a better way to do it, but I can't tell if what I'm trying is working safely or not, or I simply haven't hit a data pattern that will trigger the race condition.
Or perhaps I'm overthinking it. The algorithm in the array consists solely of some MATH and assignment statements...and a small function call that itself also consists solely of more MATH and assignment statements. Those are supposedly fully synchronous across the board. But loops are weird sometimes.
The Question
Can you use await in that manner, outside the loop itself, to trigger the code to wait for the loop to complete? Or is the only safe way to accomplish this the older manner of simply checking where you are in the loop, and not returning until you hit the end, manually.
One of the best ways to handle async and loops is to put then on a promise and wait for Promise.all remember that await returns a Promise so you can do:
async function doAllTheThings(array) {
const promises = []
array.forEach((entry, index) => {
promises.push(new Promise((resolve) => {
setTimeout(() => resolve(entry + 1), 200 )
}))
});
return Promise.all(promises)
}
async function main () {
const arrayPlus1 = await doAllTheThings([1,2,3,4,5])
console.log(arrayPlus1.join(', '))
}
main().then(() => {
console.log('Done the async')
}).catch((err) => console.log(err))
Another option is to use generators but they are a little bit more complex so if you can just save your promises and wait for then that is an easier approach.
About the question at the end:
Can you use await in that manner, outside the loop itself, to trigger the code to wait for the loop to complete? Or is the only safe way to accomplish this the older manner of simply checking where you are in the loop, and not returning until you hit the end, manually.
All javascript loops are synchronous so the next line will wait for the loop to execute.
If you need to do some async code in loop a good approach is the promise approach above.
Another approach for async loops specially if you have to "pause" or get info from outside the loop is the iterator/generator approach.
I'm working with observables and the flatMap operator, I wrote a method which makes and API call and returns an observable with an array of objects.
Basically what I need is to get that array of objects and process each object, after all items are processed. I want to chain the result to make an extra API call with another method that I wrote.
The following code does what I need:
this.apiService.getInformation('api-query', null).first().flatMap((apiData) => {
return apiData;
}).subscribe((dataObject) => {
this.processService.processFirstCall(dataObject);
}, null, () => {
this.apiService.getInformation('another-query', null).first().subscribe((anotherQueryData) => {
this.processService.processSecondCall(anotherQueryData);
});
});
But this approach isn't optimal from my perspective, I would like to do chain those calls using flatMap but if I do the following:
this.apiService.getInformation('api-query', null).first().flatMap((apiData) => {
return apiData;
}).flatMap((dataObject) => {
this.processService.processFirstCall(dataObject);
return [dataObject];
}).flatMap((value) => {
return this.apiService.getInformation('another-api-query', null).first();
}).subscribe((value) => {
this.processService.processSecondCall(value);
});
The second API call executes once for each item on the apiData array of objects. I know I'm missing or misunderstanding something. But from the second answer of this thread Why do we need to use flatMap?, I think that the second flatMap should return the processed apiData, instead is returning each of the object items on that Array. I would appreciate the help.
Thank you.
What you want is the .do() operator, and not flatMap(). flatMap() is going to transform an event to another event, and in essence, chaining them. .do() just executes whatever you instruct it to do, to every emission in the events.
From your code, there are 2 asynchronous methods (calls to the api) , and 2 synchronous (processService). What you want to do is :
Call to the first API (async), wait for the results
Process the results (sync)
Call to the second API (async), wait for the results to come back
Process the results (sync)
Hence your code should be :
this.apiService.getInformation('api-query', null)//step1
.first()
.do((dataObject) => this.processFirstCall(dataObject))//step2
.flatMap(() => this.apiService.getInformation('another-api-query', null))//step3
.first()
.do(value => this.processService.processSecondCall(value))//step4
.subscribe((value) => {
console.log(value);
});
I wrote in comment the steps corresponding to the list above. I also cleaned up unnecessary operators (like your first flatMap is kinda redundant).
Also, in the event you want to transform your data, then you should use .map() instead of .do(). And I think that is the place where you are confused with .map() and .flatMap().
The issue I think your encountering is that flatmap should be applied to an observable or promise. in your second code example, you are returning data within the flatmap operator which is then passed to the following flatmap functions, whereas these should be returning observables.
For example:
this.apiService.getInformation('api-query', null).first()
.flatMap((dataObject) => {
return this.processService.processFirstCall(dataObject);
}).flatMap((value) => {
return this.apiService.getInformation('another-api-query', null)
}).subscribe((value) => {
this.processService.processSecondCall(value);
});
See this post for futher clarification.