Emil-Sorin Malai
Emil-Sorin Malai

Reputation: 23

Is there a better way to chain .subscribe() calls?

I have this code:

this.categoriesSub = this.categoriesService.getCategories().subscribe(
      categories => {
        // This line is reached on the second run
        this.postsSub = this.postsService.getPosts().subscribe(
          posts => {
            // This line is NOT reached on the second run
            posts.forEach(p => p.category = categories.find(c => c.id === p.categoryId));
            this.posts = posts;
          }
        );
      }
    );

Here I'm getting a bunch of posts and for each post, I'm also populating it's category field.

It works fine the first time the component is loaded but if I navigate to another route and then hit the back button in the browser to return to this component it doesn't fetch the posts anymore.

Is there a better way to chain .subscribe() calls? I've seen examples using switchMap but I don't know how I would implement that here. I've tried using a forkJoin like this:

forkJoin(
      this.categoriesService.getCategories(),
      this.postsService.getPosts()
    ).subscribe(([categories, posts]) => console.log(categories));

... just to see if I get the data, but it doesn't work.

Edit: To be more precise: The problem is that on the second run, the callback of the second .subscribe() call is not executed. There are no errors, no nothing. It looks like it's just skipped. I've added a comment on the line that is not executed.

Later Edit: Here is the posts service:

export class PostsService {
  postsCollection: AngularFirestoreCollection<Post>;
  posts: Observable<Post[]>;

  constructor(private firestore: AngularFirestore) {
    this.postsCollection = this.firestore.collection('posts');

    this.posts = this.postsCollection.snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as Post;
        const id = a.payload.doc.id;
        return { id, ...data };
      }))
    );
  }

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

  getPost(id: string): Observable<Post> {
    return this.posts.pipe(
      map(posts => posts.find(p => p.id === id))
    );
  }
}

And here I use this.posts in the template:

<app-post-item *ngFor="let post of posts" [post]="post"></app-post-item>

And the post item:

<div class="post-item" @fade>
    <span class="badge post-category">{{ post.category.name }}</span>
    <a [routerLink]="['./', post.id]" class="post-title text-strong">{{ post.title }}</a>
    <span class="post-date text-medium">{{ post.date.seconds * 1000 | date }}</span><br>
    <img *ngIf="post.coverUrl" class="post-cover-img" src="{{ post.coverUrl }}" alt="none">
    <p class="post-body">{{ postBody }}</p>

    <div class="actions">
        <a [routerLink]="['./', post.id]" class="read-more-button strong">Read More!</a>
    </div>
</div>

Upvotes: 1

Views: 417

Answers (2)

Valeriy Katkov
Valeriy Katkov

Reputation: 40552

The forkJoin emits only when all observables are completed. Try to use zip instead. But you need to be sure that both observables emit a value or a error. If getCategories and getPosts are network requests, zip is better than switchMap, because the requests will be executed in parallel.

zip(
  this.categoriesService.getCategories(),
  this.postsService.getPosts()
).subscribe(([categories, posts]) => {
    posts.forEach(p => p.category = categories.find(c => c.id === p.categoryId));
});

The root of the problem is that your services share AngularFire observables, which are hot by design. The getPosts() and getCategories() methods should create new observables instead, like:

export class PostsService {
  postsCollection: AngularFirestoreCollection<Post>;

  constructor(private firestore: AngularFirestore) {
    this.postsCollection = this.firestore.collection('posts');
  }

  getPosts(): Observable<Post[]> {
    return this.postsCollection.snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as Post;
        const id = a.payload.doc.id;
        return { id, ...data };
      }))
    );
  }
}

Also, you shouldn't use subscribe/unsubscribe Observable methods directly. It's very easy to forget to unsubscribe. Use async pipe instead, which unsubscribes automatically, when the component is destroyed.

@Component({
  template: `<app-post-item *ngFor="let post of posts$ | async [post]="post"></app-post-item>`,
})
class MyComponent {
  readonly posts$: Observable<Post[]>;

  constructor(
    postsService: PostsService,
    categoriesService: CategoriesService
  ) {
    this.posts$ = zip(
      categoriesService.getCategories(),
      postsService.getPosts()
    ).pipe(
      map(([categories, posts]) => {
      posts.forEach(p => p.category = categories.find(c => c.id === p.categoryId));
      return posts;
    }));
  }
}

Upvotes: 2

Emil-Sorin Malai
Emil-Sorin Malai

Reputation: 23

The problem was that I was subscribing again to the postsService in another component but without calling .unsubscribe() in the ngOnDestroy() method.

That's why only that call would not work.

Upvotes: 0

Related Questions