0

I'm fairly new to Angular 9, and essentially I am wondering how do I return an array of objects from an async source? I currently have a firestore query in which the documents that are returned have a reference whose data I wish to render.

ngOnInit

this.currentUserRef.collection('links', ref => ref.where('pendingRequest', '==', false)).get()
            .pipe(map(querySnap => {
                const ret = [];
                querySnap.forEach(doc => {
                    const val = doc.get('otherUser').get().then(userData => {return userData});
                    ret.push(val);
                });
                return ret;
            })).subscribe(val => {
                this.links = val;
        });

HTML

<ion-item *ngFor="let link of links | async">
            <ion-avatar slot="start">
                <img [src]="getImage(link.get('profilepic'))">
            </ion-avatar>
            <ion-label>
                <h2>{{link.get('name')}}</h2>
                <p>{{link.get('bio')}}</p>
            </ion-label>
            <ion-checkbox id="{{link.id}}"></ion-checkbox>
        </ion-item>

This currently returns the error: InvalidPipeArgument: '[object Promise]' for pipe 'AsyncPipe' And when I remove the async pipe in the HTML it returns a zoneawarepromise.

EDIT

this.links = this.currentUserRef.collection('links', ref => ref.where('pendingRequest', '==', false)).get()
            .pipe(map(querySnap => {
                const ret = [];
                querySnap.forEach(async doc => {
                    const val = await doc.get('otherUser').get().then(userData => {return userData});
                    ret.push(val);
                });
                return ret;
            }));

This now works however I don't know how efficient and scalable the solution is for a large number of documents due to the ret array Note: querySnap is an object that has a built-in forEach method to loop through the array in it's docs property.

2 Answers 2

2

It's generally not advisable to mix promises and observables. They do play well together, but it jumbles and confuses your code. It's also very simple to convert a promise into an observable. Most operators that create/deal with higher-order observables will do the conversion for you. Otherwise, from(promise) returns an observable as well.

Using forkJoin

In this case, forkJoin accepts an array of Promise without complaint.

forkJoin will run all the promises concurrently and return an array of responses once/if they all complete/resolve.

this.links = this.currentUserRef.collection(
  'links', 
  ref => ref.where('pendingRequest', '==', false)
).get().pipe(
  mergeMap(querySnap => forkJoin(
    querySnap.docs.map(
      doc => doc.get('otherUser').get()
    ))
  )
);

Using concat

concat can also accept promises as parameters. We use the spread operator (...) to turn an array into a list of parameters.

This is actually closer to what you achieve with promises, as you await each promise before running the next one. This will run your promises one at a time (and not concurrently, the way forkJoin does

this.links = this.currentUserRef.collection(
  'links', 
  ref => ref.where('pendingRequest', '==', false)
).get().pipe(
  mergeMap(querySnap => concat(
    ...querySnap.docs.map(
      doc => doc.get('otherUser').get()
    ))
  ),
  toArray()
);

Aside: Cleaning up your promise code

This:

this.links = this.currentUserRef.collection('links', ref => ref.where('pendingRequest', '==', false)).get()
            .pipe(map(querySnap => {
                const ret = [];
                querySnap.forEach(async doc => {
                    const val = await doc.get('otherUser').get().then(userData => {return userData});
                    ret.push(val);
                });
                return ret;
            }));

is equivalent to

this.links = this.currentUserRef.collection(
  'links', 
  ref => ref.where('pendingRequest', '==', false)
).get().pipe(
  map(querySnap => {
    const ret = [];
    querySnap.forEach(async doc => 
      ret.push(await doc.get('otherUser').get())
    );
    return ret;
  })
)

Basically, if you're using await, you don't need .then(...

Sign up to request clarification or add additional context in comments.

6 Comments

I can't exactly test this with your code. What's your error?
So querySnap is an object that contains an array called docs which I applied the map function to and from the get('otherUser').get() I returned the custom object that contains the data I need to render. Not only is it not rendering but I am getting an error saying "You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable." when I try to render it in the HTML
That's a problem with your question perhaps? In your edit above, (The one you say works) querySnap is an array and not an object. - Also, it looks like this.links is undefined. Are you using angular's callbacks correctly? (NgOnit and such?). I'm at a loss as to why that would work for your above though
My apologies, querySnap has a built-in method called forEach() which loops through the array embedded in the object. That could be used or you could extract the docs array in the object and use traditional array looping methods. As for angular's callbacks, I am only using ngOninit currently and the code inside is the one I mentioned in my question with your fixes patched on.
"That could be used or you could extract the docs array in the object and use traditional array looping methods." - Try that? The code I wrote above works well for me. I've changed it to use querySnap.docs.map. See if that helps?
|
0

can you try this:

            .pipe(map(querySnap => {
                const ret = [];
                querySnap.forEach(doc => {
                    const val = doc.get('otherUser').get().then(userData => {return userData});
                    ret.push(val);
                });
                return ret;
            })).map(val => {
                this.links = of(val);
        });

basically you new to add an observable stream to links variable.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.