How to make an Observable from a query in Firebase?

This is my original code where the posts is only an array. My problem here is that it does not auto update on view when the data changes.

posts = [];
loadPosts() {
    this.user.getFollowedGroups().subscribe((result) => {
      this.groupsFollowed = result.following;
      this.postsCollection.ref.where('gid', 'in', this.groupsFollowed).get().then(res => {
          res.docs.forEach(doc => {
            let obj = doc.data();
            obj["id"] = doc.id;
            this.posts.push(obj);
          });
        });
    });
  }

So I am trying to save it on an observable instead. This is how I do it on other services which I dont use where query. I tried mixing it up and putting the query inside the queryFn,

followedPostsCollection: AngularFirestoreCollection<Post>;
posts: Observable<Post[]>;
loadPosts() {
    this.user.getFollowedGroups().subscribe((result) => {
      this.groupsFollowed = result.following;
      this.followedPostsCollection = this.afs.collection<Post>('announcements', ref => ref.where('gid', 'in', this.groupsFollowed));
      this.posts = this.followedPostsCollection.snapshotChanges().pipe(
        map(actions =>
          actions.map(a => {
            const data = a.payload.doc.data();
            const id = a.payload.doc.id;
            return { id, ...data };
          })
        )
      );
    });
  }

getPosts(): Observable<Post[]> {
    return this.posts;
  }

but this code gives me undefined posts when I subscribe with it. Where do I go wrong and how to make it work? Or should I just use .onSnapshot or .then like what I did on the first one but I cant seem to save it on an observable.

There is a fundamental unknowable regarding timing that is baked into this code. getPosts is returning a service property, yet that service property is assigned to in a subscription. There is no easy way for getPosts to know, yet alone do anything with the knowledge about, when that assignment is going to occur.

I painted myself into corners like this many times, and eventually settled on the following rule, which in addition to making for (IMHO) clearer code, makes it much harder to step on such rakes:

All code that interacts with futures should either produce one or consume one, but never try to do both things.

A related rule:

The first word of a future-producing function should generally be return.

If you haven’t seen this taxonomy of future-producers before, check it out. Based on its name and what it seems to be trying to do, loadPosts is in a tough spot. It’s really a category C, but it’s written like a category A. In fact, loadPosts and getPosts can’t really be disentangled, so I would combine them, and along the way totally get rid of all the instance properties in the service. There are ways to preserve them so that they make more sense, but for now I think things will be clearer if we can design away any potential sources of stale data.

I’m also going to dumb down your User class for philosophical reasons that can be discussed later if you care, but aren’t particularly germane here. Instead of the user itself generating followed groups, the service is going to do that given a POD User.

followedGroups(user: User): Observable<Group[]> {
}

postsForUser(user: User): Observable<Post[]> {
  return this.followedGroups(user).pipe(
    switchMap(groups => this.afs.collection<Post>('announcements', ref => ref.where('gid', 'in', groups).snapshotChanges()),
    map(actions => actions.map(a => {
      const data = a.payload.doc.data();
      const id = a.payload.doc.id;
      return { id, ...data };
    })));
}
1 Like

Okay thanks, I will take note of this.

I was trying to follow the tutorial here https://devdactic.com/ionic-4-firebase-angularfire/ where the firebase data was loaded in the constructor and use a function to return that observable. But as per your advise I will just merge those two and load the firebase data on function call.

And what should I do with user as parameter? For authentication or for filter? And how? Btw, I am calling the user function because I am trying to restrict myself as calling collection specific to their services. Thats why I am calling the user service to query the user collection. Or am I doing that wrong? Do enlighten me please.

I would move whatever your existing User.getFollowedGroups does out of User and into this service as Service.followedGroups(user). So, instead of:

getFollowedGroups(): Observable<Group[]> {
  return whatever.frobulate(this.foo);
}

followedGroups(user: User): Observable<Group[]> {
  return whatever.frobulate(user.foo);
}

Then perhaps I misunderstood. I assumed something called “user” represented an actual user, not a service. If in fact it’s another service, then I see no problem with the way you’ve organized it now, but I do wonder how a UserService would know how to implement something like getFollowedGroups without knowing what user we are caring about.

Sorry for the late reply. One last question, between calling firestore collection in service constructor or by calling a function, which is better and why? Because my original code calls the collection inside constructor and call the result by function call, and your answer

tells me to just merge it and call it by function call.

The clearest rule I can think of here is this:

Is it provable that everything being done in the whole getPosts/loadPosts world doesn’t change over the life of an app run? For example, can users change which groups they follow in the app? Can the user we’re concerned with change?

If it’s provable that everything is known and constant at app startup, then the service constructor is a viable option. Otherwise, I wouldn’t consider it so.

1 Like

Okay I get it now. I really learn a lot from you. Thank you.