proper usage of async and await in JS? - javascript

import Firebase from './Firebase'
import videoManager from './videoManage';
async function getAllDatabaseLocations() {
await let ref = Firebase.database().ref("locations")
var user_locations = [];
ref.on("value", function (snapshot) {
snapshot.forEach(function (datas) {
const data = datas.val();
vid_manage = new videoManager(data.videourl);
vid_ref = vid_manage.getLocationVideoUrl();
vid_ref.getDownloadURL().then(function (url) {
videourl = url;
}).catch(function (error) {
});
let lokation = data.lokation;
let videourl = data.videourl;
let openinghours = data.openinghours;
let links = data.links;
let Lokationer = {
lokation: lokation,
videoUrl: videourl,
openingshours: openinghours,
links: links
};
console.log("Location objects are: ", Lokationer);
user_locations.push(Lokationer);
// location_obj.push(Lokationer);
});
});
return user_locations;
}
export default getAllDatabaseLocations;
This method always returns an empty array, even if the console inside the loop prints as i expected? How to use async and await property so as to return an array with all Lokationer objects inside on it.

You'll need to return a new promise because of the asynchronous ref.on("value") callback.
function getAllDatabaseLocations() {
return new Promise(resolve => {
ref.on("value", function (snapshot) {
...
// when done filling the array
resolve(user_locations);
});
});
}
const userLocations = await getAllDatabaseLocations(); // user_locations

Related

Upload in Firebase Storage and fetch link to add it in Realtime Database

I'm using Firebase Realtime Database and Firebase Storage in this application, the goal is to upload in Firebase Storage the images in the pictures array, and then get the link of the Firebase Storage to that image and add it in the object which will be pushed in imagesUriArray and added to Realtime Database. The problem is that when I press addItem it successfully update the id, but the images parameter remains empty. And in fact, imagesUriArray remains empty unless I refresh the screen.
export default function NewItem({ route, navigation }) {
const [pictures, setPictures] = useState([]);
const [imagesUriArray, setImageUriArray] = useState([]);
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
})
const addItem = async () => {
uploadImages()
const changes = ref(db, path)
get(changes).then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const fetchedArray = snapshot.val().data
let array = fetchedArray;
let object = {
"id": `${Math.random()}`,
"images": imagesUriArray,
}
array.push(object)
update(changes, {
data: array
})
}
})
}
}
const uploadImages = () => {
const metadata = {
contentType: 'image/jpeg',
};
pictures.forEach( async (obj) => {
const id = obj.id
const uri = obj.uri
const response = await fetch(uri);
const blob = await response.blob();
var ref = storageUpload(storage, path)
await uploadBytes(ref, blob, metadata)
await getDownloadURL(ref)
.then((metadata) => {
let array = imagesUriArray
let object = {
"id": id,
"uri": metadata
}
array.push(object)
setImageUriArray(array)
})
.catch((error) => {
console.log(error)
});
})
}
return(
..............
)
}
Issue
This appears to be a state mutation in the uploadImages callback. A reference to the imagesUriArray state is cached locally, mutated (i.e. direct push into array), and then the same reference is saved back into state. This doesn't trigger react to rerender with any updated state value.
const uploadImages = () => {
...
pictures.forEach( async (obj) => {
const id = obj.id
const uri = obj.uri
const response = await fetch(uri);
const blob = await response.blob();
var ref = storageUpload(storage, path)
await uploadBytes(ref, blob, metadata)
await getDownloadURL(ref)
.then((metadata) => {
let array = imagesUriArray // <-- reference to state
let object = {
"id": id,
"uri": metadata
}
array.push(object) // <-- state mutation
setImageUriArray(array) // <-- same state reference
})
.catch((error) => {
console.log(error)
});
})
};
Solution
Use a functional state update to update from the previous state. Create a shallow copy of the previous state and append the new data.
const uploadImages = () => {
...
pictures.forEach( async (obj) => {
const { id, uri } = obj;
const response = await fetch(uri);
const blob = await response.blob();
const ref = storageUpload(storage, path);
await uploadBytes(ref, blob, metadata);
await getDownloadURL(ref)
.then((uri) => {
setImageUriArray(imagesUriArray => [
... imagesUriArray, // <-- shallow copy
{ id, uri }, // <-- append new object
]);
})
.catch(console.log);
})
};
Update
uploadImages needs to return a Promise so that addItem can wait for it to complete its asynchronous code. addItem also needs to access the updated imagesUriArray that uploadImages updates.
Map the pictures array to an array of Promises (i.e. an async fetchImageUri function) that eventually returns the object with id and new uri properties.
const uploadImages = () => {
...
const fetchImageUri = async ({ id, uri }) => {
try {
const response = await fetch(uri);
const blob = await response.blob();
const ref = storageUpload(storage, path);
await uploadBytes(ref, blob, metadata);
const newUri = await getDownloadURL(ref);
return { id, uri: newUrl };
} catch(error) {
console.log(error);
}
}
return Promise.all(pictures.map(fetchImageUri));
};
Update addItem to wait for the resolved array of Promises that contain the uploaded image data. Enqueue the imagesUriArray state update here, then continue the rest of the function referencing the returned uploadedImages array from uploadImages.
const addItem = async () => {
const uploadedImages = await uploadImages();
setImageUriArray(imagesUriArray => [
...imagesUriArray, // <-- shallow copy
...uploadedImages, // <-- append new objects
]);
const changes = ref(db, path);
get(changes).then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const fetchedArray = snapshot.val().data;
const object = {
id: `${Math.random()}`,
images: uploadedImages,
};
update(changes, { data: [...fetchedArray, object] });
}
});
}

Problem accessing object property created using Promise

I am not able to access the returned object property, Please tell me why its returning undefined when data is object and giving correct value.
This is function created to sendHTTPRequest based on data.
import { countryCap } from "./capitalizingFunc.js";
export const sendHTTPRequest = (country) => {
const capitalisedCountry = countryCap(country);
return fetch(
`https://covid-19-coronavirus-statistics.p.rapidapi.com/v1/total?country=${capitalisedCountry}`,
{
method: "GET",
headers: {
"x-rapidapi-key": "3b0f2e00ebmsh95246403d9540c9p1506d4jsn3c44ce26f745",
"x-rapidapi-host": "covid-19-coronavirus-statistics.p.rapidapi.com",
},
}
)
.then((response) => {
const newResponce = response.json();
return newResponce;
})
.catch((err) => {
console.error(err);
});
};
This is constructor class
export class casesDataFetcher {
constructor(countryName) {
sendHTTPRequest(countryName)
.then((response) => {
return response.data;
})
.then((data) => {
this.country = data.location;
this.cases = data.confirmed;
this.recovered = data.recovered;
this.deaths = data.deaths;
console.log(this);
return this;
});
}
}
This is execution function
import { casesDataFetcher } from "./casesDataFetcher.js";
export const screenDataShower = (country) => {
const dataStorage = [];
const globalInfected = document.querySelector(".infected h2");
const globalActive = document.querySelector(".active h2");
const globalDeaths = document.querySelector(".deaths h2");
const globalRecovered = document.querySelector(".recovered h2");
const globalCountries = document.querySelector(".countries h2");
let promise = new Promise(function (resolve, reject) {
const recordedData = new casesDataFetcher(country);
console.log(recordedData);
resolve(recordedData);
});
return promise.then((data) => {
console.log(typeof data);
console.log(typeof data);
console.log(data.cases); // NOT WORKING GIVING UNDEFINED
globalInfected.textContent = `${nn.cases}`;
globalActive.textContent = data.cases - data.recovered - data.deaths;
globalDeaths.textContent = data.deaths;
globalRecovered.textContent = data.recovered;
globalCountries.textContent = 219;
});
};
I also tried to convert the data to JSON again but still I was not able to access the property of returned data in screenDataShower
you're calling sendHTTPRequest inside casesDataFetcher's constructor, from your code there's no guarantee data is resolved when you access it
extract sendHTTPRequest into a new function and wrap into a promise
export class casesDataFetcher {
constructor(countryName) {
this.countryName = countryName
}
fetch = () => {
return new Promise((res, rej) => {
sendHTTPRequest(this.countryName)
.then((response) => {
return response.data;
})
.then((data) => {
this.country = data.location;
this.cases = data.confirmed;
this.recovered = data.recovered;
this.deaths = data.deaths;
console.log(this);
res(this);
});
})
}
}
make screenDataShower function async then you can await data from fetch function in casesDataFetcher, this way it can guarantee data is there when you access it
import { casesDataFetcher } from "./casesDataFetcher.js";
export const screenDataShower = async (country) => {
const dataStorage = [];
const globalInfected = document.querySelector(".infected h2");
const globalActive = document.querySelector(".active h2");
const globalDeaths = document.querySelector(".deaths h2");
const globalRecovered = document.querySelector(".recovered h2");
const globalCountries = document.querySelector(".countries h2");
const _casesDataFetcher = new casesDataFetcher(country)
const data = await _casesDataFetcher.fetch()
console.log(typeof data);
console.log(typeof data);
console.log(data.cases); // NOT WORKING GIVING UNDEFINED
globalInfected.textContent = `${nn.cases}`;
globalActive.textContent = data.cases - data.recovered - data.deaths;
globalDeaths.textContent = data.deaths;
globalRecovered.textContent = data.recovered;
globalCountries.textContent = 219;
};
The problem is that the json method of your response returns a promise instead of plain JSON. So you should change the call of the json method in your sendHTTPRequest function to something like:
.then((response) => {
const newResponse = response.json().then((jsonResponse) => jsonResponse);
return newResponse;
})

Resolving async function after a loop end

I expect when I call an async function to resolve promise at the end, not before.
const urls = await uploadImages({ imageChanges, questions });
// ...next step
// I will use urls
But after calling await uploadImages() it continues to run until const data = await fetch(image.src);
And then ...next step starts. How can I make it wait for imageChanges.forEach loop finish ? Should I create another nested function inside ?
const uploadImages = async ({ imageChanges, questions }) => {
if (!imageChanges.length) return null;
const storage = firebase.storage();
let urls;
try {
//** convert each new image's src from blob to downloadUrl. */
imageChanges.forEach(async image => {
const questionId = questions.findIndex(q => q.id === image.questionId);
const imagePath = `${questionId}.jpg`;
const storageRef = storage.ref(imagePath);
// **
const data = await fetch(image.src);
const blob = await data.blob();
const uploadTaskSnapshot = await storageRef.put(blob);
const downloadURL = await uploadTaskSnapshot.ref.getDownloadURL();
urls.push(downloadURL)
});
return urls;
} catch (error) {
console.log(error.message);
}
};
forEach with async doesn't work as expected. Read this answer for more info.
Try like this
const uploadImages = async ({ imageChanges, questions }) => {
if (!imageChanges.length) return null;
const storage = firebase.storage();
try {
const imageChangesUrlPromise = imageChanges.map(async () => {
const questionId = questions.findIndex(q => q.id === image.questionId);
const imagePath = `${questionId}.jpg`;
const storageRef = storage.ref(imagePath);
const data = await fetch(image.src);
const blob = await data.blob();
const uploadTaskSnapshot = await storageRef.put(blob);
const downloadURL = await uploadTaskSnapshot.ref.getDownloadURL();
return downloadURL;
})
return await Promise.all(imageChangesUrlPromise);
} catch (error) {
console.log(error.message);
}
};
and then
const urls = await uploadImages({ imageChanges, questions });
...
JavaScript does this because forEach is not promise-aware. It cannot support async and await. You cannot use await in forEach.
If you use await in a map, map will always return an array of promises. This is because asynchronous functions always return promises.
By littile modification to your code, this should work,
const uploadImages = async ({ imageChanges, questions }) => {
if (!imageChanges.length) return null;
const storage = firebase.storage();
let urls;
try {
//** convert each new image's src from blob to downloadUrl. */
await Promise.all(imageChanges.map(async image => {
const questionId = questions.findIndex(q => q.id === image.questionId);
const imagePath = `${questionId}.jpg`;
const storageRef = storage.ref(imagePath);
// **
const data = await fetch(image.src);
const blob = await data.blob();
const uploadTaskSnapshot = await storageRef.put(blob);
const downloadURL = await uploadTaskSnapshot.ref.getDownloadURL();
urls.push(downloadURL)
}));
return urls;
} catch (error) {
console.log(error.message);
}
};
const urls = await uploadImages({ imageChanges, questions });

How to wait for a forEach to finish before returning from my promise / function

Angular, firestore
I have an angular function to get products from one firestore collection, then I am looping over the results of that query to lookup prices from another collection.
How can I wait until the prices forEach is done before returning from the outer promise and the outer function itself?
The returned result contains a products array, but the prices array for each product is empty.
const products = await this.billingService.getProducts();
async getProducts() {
let result = [];
let product = {};
return this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(async function (doc) {
product = doc.data();
product['prices'] = [];
await doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
.then(function (docs) {
// Prices dropdown
docs.forEach(function (doc) {
const priceId = doc.id;
const priceData = doc.data();
product['prices'].push(priceData);
});
});
});
result.push(product);
return result;
});
}
I also tried this approach, but not sure how to access the results
await this.billingService.getProducts().then(results =>
getProducts() {
const dbRef =
this.db.collection(
'products',
ref => { ref
let query: Query = ref; return query.where('active', '==', true)
});
const dbPromise = dbRef.ref.get();
return dbPromise
.then(function(querySnapshot) {
let results = [];
let product = {};
querySnapshot.forEach(function(doc) {
let docRef = doc.ref
.collection('prices')
.orderBy('unit_amount')
results.push(docRef.get())
});
return Promise.all(results)
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
}
Posting as a Community Wiki answer, based in the comments.
For this case, using a forEach() is not the correct choice. As clarified in this case here, forEach() doesn't work properly with await functions, this way, not working correctly with your promises. Considering that and the fact that you want to read the data in sequence - as the results from one query will impact in the second one - you need to use a normal for, to loop through the data and arrays. This specific answer here should help you with code samples.
This version of the code works:
getProducts(): Promise<any> {
return new Promise((resolve,reject)=> {
let result = [];
let product = {};
this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(async function (querySnapshot:firebase.firestore.QuerySnapshot) {
for(const doc of querySnapshot.docs) {
const priceSnap = await doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
product = doc.data();
product['prices'] = [];
// Prices dropdown
for(const doc of priceSnap.docs) {
const priceId = doc.id;
let priceData = doc.data();
priceData['price_id'] = priceId;
product['prices'].push(priceData);
resolve(result);// returns when it reaches here
};
result.push(product);
};
});
})
}
Make the getProducts() function a promise. Thus it will only return when you resolve it ( or reject it).
getProducts() {
return new Promise((resolve,reject)=> {
let result = [];
let product = {};
this.db.collection(
'products',
ref => { ref
let query: Query = ref;
return query.where('active', '==', true)
})
.ref
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(async function (doc) {
product = doc.data();
product['prices'] = [];
doc.ref
.collection('prices')
.orderBy('unit_amount')
.get()
.then(function (docs) {
// Prices dropdown
docs.forEach(function (doc) {
const priceId = doc.id;
const priceData = doc.data();
product['prices'].push(priceData);
});
resolve(result);// returns when it reaches here
});
});
result.push(product);
});
})
}
Then you can call the promise using then or await
this.billingService.getProducts().then( res => {
const products = res;
})
Using await
const products = await this.billingService.getProducts();

Why I am getting undefined after calling fetchNotes

After calling fetchNotes from the addNote function it shows me undefined as push method is not defined in the addNote function
const fs = require('fs');
const fetchNotes = ()=>{
fs.readFile('data.json',(err,notes)=>{
if(err){
// return empty array if data.json not found
return [];
}else{
// return Object from data found data.json file
return JSON.parse(notes)
}
});
}
const saveNotes = (notes) =>{
fs.writeFile('data.json',JSON.stringify(notes),()=>{
console.log('Notes is successfully saved');
});
}
const addNote = (title, body)=>{
const note = {
title,
body
}
const notes = fetchNotes();
//Push method not defined
notes.push(note);
saveNotes(notes);
return note;
}
module.exports.addNote = addNote;
It returns undefined because when you are returning in the callback you are not exactly returning from the fetchNotes function itself.
Maybe you can use the readFileSync and don't use callback or maybe you can make it a promise and use async/await
const fetchNotes = () => {
return new Promise((res, rej) => {
fs.readFile('data.json', (err, notes) => {
if (err) {
// return empty array if data.json not found
res([]);
} else {
// return Object from data found data.json file
res(JSON.parse(notes));
}
});
});
}
const addNote = async (title, body) => {
const note = {
title,
body
}
const notes = await fetchNotes();
//Push method not defined
notes.push(note);
saveNotes(notes);
return note;
}
Alternatively, you can use utils.promisify
return JSON.parse(notes) does not store this value inside fetchNotes because it is asynchronous, so you get the content of the file later in time.
To do it asynchronously, you can use async/await :
const fetchNotes = () => {
return new Promise((resolve, reject) => {
fs.readFile('data.json', (err,notes) => resolve(JSON.parse(notes)));
})
}
const addNote = async (title, body) => {
// ...
const notes = await fetchNotes();
notes.push(note);
saveNotes(notes);
return note;
}
You can also do it synchronously :
const fetchNotes = () => JSON.parse( fs.readFileSync('data.json') );
const notes = fetchNotes();

Categories