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.
Related
I cannot access the specific objects inside the array. I pushed the data to the array from an api. When i console log countriesArr it shows the full array of objects with all its properties, but when I try to access the specific object in a specific index I get undefined. Also, it should return an array but the typeof shows its an object? Can anyone help?
const countriesURL = 'https://restcountries.eu/rest/v2/all';
let countriesArr = [];
async function getCountriesData() {
const response = await fetch(countriesURL);
const data = await response.json();
showCountries(data);
}
function showCountries(countries) {
countries.forEach((country) => {
countriesArr.push(country)
})}
console.log(countriesArr) //this shows array of objects
console.log(countriesArr[0]) //this shows undefined????
const countriesURL = 'https://restcountries.eu/rest/v2/all';
let countriesArr = [];
async function getCountriesData() {
const response = await fetch(countriesURL);
const data = await response.json();
showCountries(data);
}
function showCountries(countries) {
countries.forEach((country) => {
countriesArr.push(country)
})}
getCountriesData();
console.log(countriesArr) //this shows array of objects
console.log(countriesArr[0]) //this shows undefined????
JavaScript is single threaded-- whether it's running in your browser or NodeJS. So when the Browser processes your code, it will process top>down and evaluate variables in the starting file and proceed to put all functions in the callstack.
When the Browser Engine comes across an asynchronous function, it takes a reference to that function and puts that function into a callback queue, and if it's a promise(which async is since it's just a wrapper on promises), it gets put into a "job queue"/"microtask queue". Once the asynchronous function emits a complete event, the Browser Engine takes that data and pushes that function alongside its reference into the callstack to be run after all current tasks/functions in the stack are done completing. The function reference contains the line number so the engine knows where to evaluate the function.
So knowing this, when you get to getCountriesData(), this asynchronous function gets popped off the main thread, and put into a separate queue that contains threads dedicated for networking related tasks, and program execution immediately processes and evaluates the following console logs before the getCountriesData() finishes executing.
The only way for you to be sure you're correctly inserting/manipulating the data you received from the getCountriesData() into the countriesArr is if you're doing all the data related tasks within that function itself OR in then() proceeding it.
your main problem is async fetching of data, so you make request to the server and while it is making, your next code is compeleting
getCountriesData() is loading...
showCountries(countries) complete!
console.log(countriesArr) complete!
console.log(countriesArr[0]) complete!
In this case countries array is empty and when you take [0] element which is not exists, JS return 'undefined'.
Try to use Promise API to avoid this:
const countriesURL = 'https://restcountries.eu/rest/v2/all';
let countriesArr = [];
async function getCountriesData() {
const response = await fetch(countriesURL);
const data = await response.json();
showCountries(data);
}
function showCountries(countries) {
countries.forEach((country) => {
countriesArr.push(country)
})
}
/* this function return special type Promise */
getCountriesData().then(res => {
console.log(countriesArr)
console.log(countriesArr[0])
})
I've been stuck on this for quite a while now and have no idea what's going on.
I'm running the following function:
const getEmployees = async () => {
const query = await db.collection('employees')
const employees = query.onSnapshot(function (querySnapshot) {
const employeeArr = [];
querySnapshot.forEach(function (doc) {
employeeArr.push(doc.data());
});
console.log(1, employeeArr)
return employeeArr;
})
console.log(2, employees)
return employees
}
the console.log(employeeArr) shows me exactly what I want to retrieve: an array with objects that consist of employee details (name, birhtday). However when I return this array and console.log(employees) after it logs the following:
ƒ () {
i.Xl(), r.os.ds(function () {
return Rr(r.B_, o);
});
}
This (2) is also logged before the array (1).
I've been following the documentation on firestore and so far so good but I can't wrap my head around this. If I call getEmployees() and store the value it returns it also shows the unction. Anyone have any idea why and how I can fix it? Thanks!
You're saving inside the employees variable the result of query.onSnapshot().
Also with onSnapshot() the callback is called everytime there is a data update in the collection, wathing at your code I think that you simply want to get data once and to do so you just need to:
const employees = await query.get();
Reference here: https://firebase.google.com/docs/firestore/query-data/get-data
You can't mix the use of promises with realtime database listeners. They're not compatible. Functions that return a promise (like any async function) are meant to return a single result asynchronously. onSnapshot does not return a promise. Instead, it invokes the provided callback whenever the results of the query change.
If you want realtime results, you're going to have to deal with the snapshot listeners correctly. onSnapshot returns a function that you call to remove the listener when you're done with it. You can see an example in the documentation:
var unsubscribe = db.collection("cities")
.onSnapshot(function (){
// Respond to data
// ...
});
// Later ...
// Stop listening to changes
unsubscribe();
Failing to remove this listener when you're done means that you will leak the listener, and you will be charged for its reads over time. That's bad.
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.
so basically i have a web application that retrieves data from firebase. and since it takes a lot of time to retrieve data from firebase, i used promise to make my code to populate at the right time. here is my code:
var promise = getDataFirebase();
promise.then(function () {
console.log(Collect);
console.log("firsst");
return getDataFirebaseUser();
}).then(function () {
console.log("Second");
});
function getDataFirebase() {
return new Promise(function (resolve, reject) {
refReview.on("value", function (snap) {
var data = snap.val();
for (var key in data) {
Collect.push({
"RevieweeName": data[key].revieweeID.firstname.concat(" ", data[key].revieweeID.lastname),
"ReviewerName": data[key].reviewerID.firstname.concat(" ", data[key].reviewerID.lastname),
rating: data[key].rating,
content: data[key].content,
keyOfReviewee: data[key].revieweeID.userID
})
var getDataToUsers = firebase.database().ref("users").child(data[key].revieweeID.userID);
getDataToUsers.once("value", async function (snap) {
var fnLn = snap.val();
var first = fnLn.isTerminated;
console.log("terminateStatus", first);
});
}//end of for loop
resolve();
}); //end of snap
});
}
so in function getDataFirebase, data is retrieved from firebase and is push to an array called , Collect. So, after pushing 1 row, it query again to the firebase for another table of data then continues the loop. The problem here is that, i wanted to finish all processes before it resolves the promise.
the output of console according to the code is as follow:
Collect (array)
first
Second
terminateStatus, 1
it must be
Collect (array)
first
terminateStatus,1
second
I'm not 100% sure how your code is working but it looks like you're doing the right thing on refReview.on("value"): creating a promise before calling Firebase, and then resolving it afterwards. But you're not doing that on getDataToUsers.once("value"). There, you're firing the event and not waiting for it to return — the for-loop continues on, and all the callbacks are processed later, but resolve is at the end of the for-loop, so it's too late by then.
My guess is that you thought the async keyword would cause the for-loop to wait for that job to complete, but actually all it does here is cause the callback to return a promise — which is ignored by the on function.
You have a few options, but probably the best will be to use Promise.all, which accepts an array of promises and waits for them all to be resolved — but you should probably use Firebase's Promise API rather than bolting promises onto the event API. So it'll look something like:
refReview.once('value')
.then(snap => Promise.all(snap.val().map(item =>
firebase.database()
.ref("users")
.child(item.revieweeID.userID)
.once('value')
.then(snap => {
console.log('terminated');
])
})));
(except with the actual functionality added in, natch)
I will appreciate if you help me with the following case:
Given function:
async function getAllProductsData() {
try {
allProductsInfo = await getDataFromUri(cpCampaignsLink);
allProductsInfo = await getCpCampaignsIdsAndNamesData(allProductsInfo);
await Promise.all(allProductsInfo.map(async (item) => {
item.images = await getProductsOfCampaign(item.id);
}));
allProductsInfo = JSON.stringify(allProductsInfo);
console.log(allProductsInfo);
return allProductsInfo;
} catch(err) {
handleErr(err);
}
}
That function is fired when server is started and it gathers campaigns information from other site: gets data(getDataFromUri()), then extracts from data name and id(getCpCampaignsIdsAndNamesData()), then gets products images for each campaign (getProductsOfCampaign());
I have also express.app with following piece of code:
app.get('/products', async (req, res) => {
if (allProductsInfo.length === undefined) {
console.log('Pending...');
allProductsInfo = await getAllProductsData();
}
res.status(200).send(allProductsInfo);
});
Problem description:
Launch server, wait few seconds until getAllProductsData() gets executed, do '/products' GET request, all works great!
Launch server and IMMEDIATELY fire '/products' GET request' (for that purpose I added IF with console.log('Pending...') expression), I get corrupted result back: it contains all campaigns names, ids, but NO images arrays.
[{"id":"1111","name":"Some name"},{"id":"2222","name":"Some other name"}],
while I was expecting
[{"id":"1111","name":"Some name","images":["URI","URI"...]}...]
I will highly appreciate your help about:
Explaining the flow of what is happening with async execution, and why the result is being sent without waiting for images arrays to be added to object?
If you know some useful articles/specs part covering my topic, I will be thankful for.
Thanks.
There's a huge issue with using a global variable allProductsInfo, and then firing multiple concurrent functions that use it asynchronously. This creates race conditions of all kinds, and you have to consider yourself lucky that you got only not images data.
You can easily solve this by making allProductsInfo a local variable, or at least not use it to store the intermediate results from getDataFromUri and getCpCampaignsIdsAndNamesData - use different (local!) variables for those.
However, even if you do that, you're potentially firing getAllProductsData multiple times, which should not lead to errors but is still inefficient. It's much easier to store a promise in the global variable, initialise this once with a single call to the info gathering procedure, and just to await it every time - which won't be noticeable when it's already fulfilled.
async function getAllProductsData() {
const data = await getDataFromUri(cpCampaignsLink);
const allProductsInfo = await getCpCampaignsIdsAndNamesData(allProductsInfo);
await Promise.all(allProductsInfo.map(async (item) => {
item.images = await getProductsOfCampaign(item.id);
}));
console.log(allProductsInfo);
return JSON.stringify(allProductsInfo);
}
const productDataPromise = getAllProductsData();
productDataPromise.catch(handleErr);
app.get('/products', async (req, res) => {
res.status(200).send(await productDataPromise);
});
Of course you might also want to start your server (or add the /products route to it) only after the data is loaded, and simply serve status 500 until then. Also you should consider what happens when the route is hit after the promise is rejected - not sure what handleErr does.