How to iterate through a Firestore snapshot documents while awaiting - javascript

I have been trying to obtain a series of documents from firestore, reading them and acting accordingly depending on a series of fields. The key part is I want to wait for a certain process while working on each document. The official documentation presents this solution:
const docs = await firestore.collection(...).where(...).where(...).get()
docs.forEach(await (doc) => {
//something
})
The problem with this solution is taht when you have a promise inside the forEach it won't await it before continuing, which I need it to. I have tried using a for loop:
const docs = await firestore.collection(...).where(...).where(...).get()
for(var doc of docs.docs()) {
//something
}
When using this code Firebase alerts that 'docs.docs(...) is not a function or its return value is not iterable'. Any ideas on how to work around this?

Note that your docs variable is a QuerySnapshot type object. It has an array property called docs that you can iterate like a normal array. It will be easier to understand if you rename the variable like this:
const querySnapshot = await firestore.collection(...).where(...).where(...).get()
for (const documentSnapshot of querySnapshot.docs) {
const data = documentSnapshot.data()
// ... work with fields of data here
// also use await here since you are still in scope of an async function
}

I have found this solution.
const docs = [];
firestore.collection(...).where(...).get()
.then((querySnapshot) => {
querySnapshot.docs.forEach((doc) => docs.push(doc.data()))
})
.then(() => {
docs.forEach((doc) => {
// do something with the docs
})
})
As you can see this code stores the data in an extern array and only after this action it works with that data
I hope this helped you to solve the problem!

Related

Loading data from Firebase asynchronously

I am developing a business manager web app in React with Firebase back-end. I am also coding a local API to simplify Firebase functions. I created this method, which loads data from a Firebase collection and returns an array of documents.
getDocuments(collection) {
var documents = [];
firebase.firestore().collection(collection).get().then(snapshot => {
snapshot.forEach(doc => {
documents.push(doc);
});
}).then(() => {
return documents;
})
}
However, when I call the method and assign it to a variable which I later print to the console, it says undefined.
var employees = getDocuments("employees");
console.log(employees);
What I want to do is to use .then() after calling the method to print the already loaded data to the console. Something like this:
var employees = getDocuments("employees").then(response => {
console.log(response);
})
Any help would be appreciated. Thank you.
Your getDocuments function seems to be unnecessarily complicated. This:
getDocuments(collection) {
return firebase.firestore().collection(collection).get().then(snapshot=>snapshot.docs)
}
yields exactly the same intended result (an Array of docs wrapped in a promise) as your function but performs faster since it skips looping through all the documents in the snapshot https://firebase.google.com/docs/reference/js/firebase.firestore.QuerySnapshot#docs
Afterwards just extract the value from the Promise returned by this function in your preferred way:
Option 1 (async/await)
let employees= await getDocuments('employees')
console.log(employees)
Option 2 (chaining)
let employees =[]
getDocuments('employees').then(response => {
employees=response
console.log(employees)
})
Explanation
When you are doing this:
var employees = getDocuments("employees").then(response => {
console.log(response);
})
you aren't receiving any value from getDocuments since you didn't return anything in the first place.
getDocuments(collection) {
var documents = [];
firebase.firestore().collection(collection).get().then(snapshot => {
snapshot.forEach(doc => {
documents.push(doc);
});
}).then(() => {
return documents; <-- this is returning the value of documents to the parent scope which is 'getDocuments', since the 'then' is related to the 'get' function
})
}
You should assign employees in the then like this
var employees = [];
getDocuments("employees").then(response => {
employees = response;
console.log(response);
})
Or if you are in an asynchronous function, you could go for something like this
var employees = await getDocuments("employees");
console.log(employees);
But the await keyword has to be done in an async function

Recursion with an API, using Vanilla JS

I'm playing with the Rick and Morty API and I want to get all of the universe's characters
into an array so I don't have to make more API calls to work the rest of my code.
The endpoint https://rickandmortyapi.com/api/character/ returns the results in pages, so
I have to use recursion to get all the data in one API call.
I can get it to spit out results into HTML but I can't seem to get a complete array of JSON objects.
I'm using some ideas from
Axios recursion for paginating an api with a cursor
I translated the concept for my problem, and I have it posted on my Codepen
This is the code:
async function populatePeople(info, universePeople){ // Retrieve the data from the API
let allPeople = []
let check = ''
try {
return await axios.get(info)
.then((res)=>{
// here the current page results is in res.data.results
for (let i=0; i < res.data.results.length; i++){
item.textContent = JSON.stringify(res.data.results[i])
allPeople.push(res.data.results[i])
}
if (res.data.info.next){
check = res.data.info.next
return allPeople.push(populatePeople(res.data.info.next, allPeople))
}
})
} catch (error) {
console.log(`Error: ${error}`)
} finally {
return allPeople
}
}
populatePeople(allCharacters)
.then(data => console.log(`Final data length: ${data.length}`))
Some sharp eyes and brains would be helpful.
It's probably something really simple and I'm just missing it.
The following line has problems:
return allPeople.push(populatePeople(res.data.info.next, allPeople))
Here you push a promise object into allPeople, and as .push() returns a number, you are returning a number, not allPeople.
Using a for loop to push individual items from one array to another is really a verbose way of copying an array. The loop is only needed for the HTML part.
Also, you are mixing .then() with await, which is making things complex. Just use await only. When using await, there is no need for recursion any more. Just replace the if with a loop:
while (info) {
....
info = res.data.info.next;
}
You never assign anything to universePeople. You can drop this parameter.
Instead of the plain for loop, you can use the for...of syntax.
As from res you only use the data property, use a variable for that property only.
So taking all that together, you get this:
async function populatePeople(info) {
let allPeople = [];
try {
while (info) {
let {data} = await axios.get(info);
for (let content of data.results) {
const item = document.createElement('li');
item.textContent = JSON.stringify(content);
denizens.append(item);
}
allPeople.push(...data.results);
info = data.info.next;
}
} catch (error) {
console.log(`Error: ${error}`)
} finally {
section.append(denizens);
return allPeople;
}
}
Here is working example for recursive function
async function getAllCharectersRecursively(URL,results){
try{
const {data} = await axios.get(URL);
// concat current page results
results =results.concat(data.results)
if(data.info.next){
// if there is next page call recursively
return await getAllCharectersRecursively(data.info.next,results)
}
else{
// at last page there is no next page so return collected results
return results
}
}
catch(e){
console.log(e)
}
}
async function main(){
let results = await getAllCharectersRecursively("https://rickandmortyapi.com/api/character/",[])
console.log(results.length)
}
main()
I hesitate to offer another answer because Trincot's analysis and answer is spot-on.
But I think a recursive answer here can be quite elegant. And as the question was tagged with "recursion", it seems worth presenting.
const populatePeople = async (url) => {
const {info: {next}, results} = await axios .get (url)
return [...results, ...(next ? await populatePeople (next) : [])]
}
populatePeople ('https://rickandmortyapi.com/api/character/')
// or wrap in an `async` main, or wait for global async...
.then (people => console .log (people .map (p => p .name)))
.catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>/* dummy */ const axios = {get: (url) => fetch (url) .then (r => r .json ())} </script>
This is only concerned with fetching the data. Adding it to your DOM should be a separate step, and it shouldn't be difficult.
Update: Explanation
A comment indicated that this is hard to parse. There are two things that I imagine might be tricky here:
First is the object destructuring in {info: {next}, results} = <...>. This is just a nice way to avoid using intermediate variables to calculate the ones we actually want to use.
The second is the spread syntax in return [...results, ...<more>]. This is a simpler way to build an array than using .concat or .push. (There's a similar feature for objects.)
Here's another version doing the same thing, but with some intermediate variables and an array concatenation instead. It does the same thing:
const populatePeople = async (url) => {
const response = await axios .get (url)
const next = response .info && response .info .next
const results = response .results || []
const subsequents = next ? await populatePeople (next) : []
return results .concat (subsequents)
}
I prefer the original version. But perhaps you would find this one more clear.

Data disappearing after async http calls in Angular

I have to code a tree component that displays multiple data, which worked fine with mocked data for me. The problem here is when I try to get data from servers, let me explain:
I have three main objects : Districts, buildings and doors. As you may guess, doors refers to buildingId and buildingID to districts. So to retrieve data and create my tree nodes, I have to do some http calls in forEach loops which is not asynchronous.
I won't share with you everything but just a minimized problem so I can get help easily:
This method retrieves a district array from the server and puts it in a local array:
async getDistricts(){
this.districtDataService.getDistrictData().toPromise().then(async districts => {
this.districts = await districts.results as District[];
});
}
On my ngOnInit :
ngOnInit() {
this.getDistricts().then(async () => {
console.log(this.districts);
for (const district of this.districts){
console.log(district);
}
})
The first console.log (in NgOnInit) returns an empty array, which is quite surprising because the first method puts the data in "this.districts". and logging data in the first method just after I put it in returns an array with my data. I guess it have something to do with the async/await I've used. Can anyone help?
EDIT 1: Tried to use this.getDistricts().finally() instead of this.getDistricts().then(), but didn't work.
EDIT 2: console.log in getDistrict get executed after the one before my loop. The expected behavior would be the opposite.
SOLVED: putting the for loop in a finally block after my HTTP call solves this. So as the answer says, I think I'm over engineering the async/await calls. I have to rethink my work based on this. Thank you everyone!
Well, you should return your Promise from getDistricts. Also you are very much over engineering and complicating the async/await concept. I understand you don't want to use Observables, but I would advise you to use them anyways.
With promises and async/await so you kinda see how to use them:
async getDistricts(): Promise<District[]> {
const { results } = await this.districtDataService.getDistrictData();
return results;
}
async ngOnInit(): Promise<void> {
this.districts = await this.getDistricts();
for (const district of this.districts){
console.log(district);
}
}
With Observable it would look like this:
getDistricts(): Observable<District[]> {
return this.districtDataService.getDistrictData().pipe(
map(({ results }) => results as District[])
);
}
ngOnInit(): void {
this.getDistricts().subscribe((districts) => {
this.districts = districts;
for (const district of this.districts){
console.log(district);
}
});
}
Just to provide whoever needs to make multiple http calls in a desired order.
as mentionned by others, i overcomplicated the concept of async await.
The trick was to use observables, convert them to Promises using .toPromise(), using .then() to get data into my variables, then making other async calls in finally block using .finally(async () => { ... }).
here's what my final code looks like :
async ngOnInit(): Promise<void> {
await this.districtDataService.getDistrictData().toPromise().then(response => {
this.districts = response.results as District[];
console.log(this.districts);
}).finally(async () => {
for (const district of this.districts){
await this.districtDataService.getBuildingsOfDistrict(district.id).toPromise().then(response => {
this.buildings = response.results as Building[];
console.log(this.buildings);
}).finally(async ()=> {
for(const building of this.buildings){
await this.districtDataService.getDoorsOfBuilding(building.id).toPromise().then(response => {
this.doors = response.results as Door[];
console.log(this.doors);
}).finally(async () => {
for(const door of this.doors){
await this.doorNodes.push(new districtNodeImpl(false,null,null,door,null));
}
})
await this.buildingNodes.push(new districtNodeImpl(false,null,building,null,this.doorNodes));
}
})
await this.dataSource.push(new districtNodeImpl(false,district,null,null,this.buildingNodes));
console.log(this.dataSource);
this.buildingNodes = new Array();
this.doorNodes = new Array();
}
})
Hope this will help ! have a nice day.

React Hooks, setState not working with an async .map call

So, I'm trying to change a state in my component by getting a list of users to call an api.get to get data from these users and add on an new array with the following code:
function MembersList(props) {
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => {
return dev.login;
});
handleDevs(arr);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [membersList]);
function handleDevs(membersArr) {
membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
setDevs([...devs, data]);
});
}
but the devs state always return an empty array, what can I do to get it to have the actual users data on it?
The issue you were having is because you were setting devs based on the data from the original render every time due to the closure around handleDevs function. I believe this should help take care of the issues you were having by using the callback method of using setDevs.
This also takes care of some issues with the dependency arrays and staleness in the useEffect hook. Typically using // eslint-disable-next-line react-hooks/exhaustive-deps should be your last resort.
function MembersList(props) {
// this isn't needed unless you are using it separately
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => dev.login);
arr.forEach(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
setDevs((devs) => [...devs, data]);
})
}, [membersList]);
}
You need to understand how React works behind scenes.
In short, it saves all the "sets" until it finishes the cycle and just after that actually update each state.
I think that why you do not see the current state updated.
For better understanding read this post: Medium article
The issue is that setDevs uses devs which is the version of devs when handleDevs is defined. Therefore setDevs will really only incorporate the data from the last time setDevs is called.
To fix this you can use the callback version of setDevs, like so:
setDevs(prevDevs => [...prevDevs, data])
Also since you are not trying to create a new array, using map is not semantically the best loop choice. Consider using a regular for loop or a forEach loop instead.
you call setDevs in the async execution loop. Here is the updated handleDevs function.
function handleDevs(membersArr) {
const arr = membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
return data;
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
});
setDevs([...devs, ...arr]);
}

A cleaner way to query a collection and return the data as a list?

It seems like there is a cleaner and more optimised way to query a Firestore collection, call doc.data() on each doc, and then return an array as result. The order in which the docs are pushed into the result array feels haphazard.
There are many steps to this code:
A new result variable is created
A query is made to retrieve the 'stories' collection
For each doc, we call the doc.data()
We push each doc to the result array
Return the result array
function getStories() {
var result = [];
db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
})
return result;
}
The code works fine but it seems like we can write this code in a cleaner way with less steps.
The code you shared actually doesn't work, as George comment. Since Firestore loads data asynchronously, you're always returns the array before the data has been loaded. So your array will be empty.
In code:
function getStories() {
var result = [];
db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
})
return result;
}
var result = getStories();
console.log(result.length);
Will log:
0
To fix this, you want to return a promise, which resolves once the data has loaded. Something along these lines:
function getStories() {
var result = [];
return db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
return result;
})
}
So this basically added two return statements, which makes your result bubble up and then be returned as the promise from getStories. To invoke this, you'd do:
getStories().then(result => {
console.log(result.length);
})
Which will then log the correct number of results.
First, define a map function for use with firebase (named mapSnapshot because it is meant specifically for use with firebase):
const mapSnapshot = f => snapshot => {
const r = [];
snapshot.forEach(x => { r.push(f(x)); });
return r;
}
Then, you can just work with Promise and mapSnapshot:
function getStories() {
return db.collection('stories').
get().
then(mapSnapshot(doc => doc.data()));
}
Usage example:
getStories().then(docs => ... do whatever with docs ...)
To be honest, it isn't less code if used as a one-time solution. But the neat thing here is, it allows to create reusable abstractions. So for example, you could use mapSnapshot to create a snapshotToArray function which can be reused whenever you need to convert the DataSnapshot from firebase to a normal Array:
const snapshotToArray = mapSnapshot(x => x);
Please note: This doesn't depend on any collection name whatsoever. You can use it with any DataSnapshot from any collection to convert it into an Array!
That's not all. You can just as easily create a function from the contents of a DataSnapshot into a regular Array with the contents in it:
const readDocsData = mapSnapshot(x => x.data());
Again, this doesn't look like a big deal – until you realize, you can also create a fromFirebase function, which allows to query various datasets:
const fromFirebase = (name, transform = x => x) => {
return db.collection(name).get().then(transform);
}
And which you can then use like this:
fromFirebase('stories', readDocsData).then(docs => {
// do what you want to do with docs
});
This way, the final result has less steps you as a programmer notice immediatly. But it produced (although reusable) several intermediate steps, each hiding a bit of abstraction.

Categories