Search code examples
angularrxjsrxjs-observablesrxjs-subscriptions

RxJS subscription in Angular component


I don't completely understand where and how I need to declare observables / subjects in Angular component.

Currently I develop a website which interacts with MovieDB API and I have everything working, but at the same time I understand that my code is bad, because there aren`t cleanups of subscriptions after destroying the component, but to do these cleanups I need at least understand how to use RxJS correctly.

I think that my usage is incorrect, because I have new subscription on every interaction with the page. Also as I understand they need to be declared in constructor.

The idea of this page is that there is an input form where user types the query and checks the radio-button to choose what to search: 'tv' or 'movies'. When there are the results of searching, there appears a button to expand the results.

So, here is the code:

import {Component, OnDestroy} from '@angular/core';
import {ISearchParams} from '../../models/search-params.interface';
import {ShowService} from '../../services/show.service';
import {IResultsIds} from '../../models/results.interface';
import {distinctUntilChanged} from 'rxjs/operators';
import {Subscription} from 'rxjs';

@Component({
  selector: 'app-search',
  templateUrl: './search-tab.component.html',
  styleUrls: ['./search-tab.component.scss']
})
export class SearchTabComponent implements OnDestroy {

  searchParams!: ISearchParams;

  searchResults!: IResultsIds;

  searchSub!: Subscription;
  showMoreSub!: Subscription;

  constructor(private movieService: ShowService) {
  }

  search(searchParams: ISearchParams): void {
    this.searchParams = searchParams;

    this.searchSub = this.movieService.search(searchParams)
      .pipe(distinctUntilChanged())
      .subscribe(results => {
        this.searchResults = results;
      });
  }

  showMore(): void {
    if (!this.isFinished()) {
      this.searchParams.page++;

      this.showMoreSub = this.movieService.search(this.searchParams)
        .subscribe(results => {
          this.searchResults!.ids.push(...results.ids);
        });
    }
  }

  isFinished = () => this.searchParams.page >= this.searchResults!.total_pages;

  ngOnDestroy(): void {
    // this.searchSub.unsubscribe();
    // this.showMoreSub.unsubscribe();
  }
}

And the HTML:

<main class="main-content search-container">
  <app-search-form (searchParams)="search($event)"></app-search-form>
  <div class="results" *ngIf="searchResults">
    <app-show-description *ngFor="let id of searchResults.ids"
                          [showType]="searchParams.type"
                          [showId]="id"></app-show-description>
  </div>
  <button *ngIf="searchResults && !isFinished()"
          (click)="showMore()"
          class="load-btn more-btn">show more...</button>
</main>

I will be very grateful if you help me and tell where I made mistakes. On more time, everything works this way, but I want to understand usage of RxJS.

entering the input and getting the results

expanding button

UPD @H3AR7B3A7 thank you very much, you have clearified my knowledge a bit! but now I have another misunderstanding: how to transform 2 array observables into one? I have googled but I can't find the solution to my problem: I have function to load more movies - it has to extend the array of distinctMovies$, which looks just like an array of numbers (ids), but I can't join two arrays, all I get is [...], [...] but not [......]

I tried this:

showMore(): void {
    this.searchParams.page++;

    this.distinctMovies$ = concat(this.distinctMovies$,
      this.movieService.search(this.searchParams)
        .pipe(pluck('ids')))
      .pipe(tap(console.log));
  }

Solution

  • It looks like you understand the basics to me.

    Although, you don't need to declare anything in the constructor.

    Also as I understand they need to be declared in constructor.

    You usually only use the constructor to inject services like you are doing already.

    You might want to use the ngOnInit() method to declare your initial state of the component:

    export class SearchTabComponent implements OnInit {
      distinctMovies$!: Observable<Movie[]>
    
      ngOninit(): void {
        //...
      }
    }
    

    You can solve a lot of your problems by just never subscribing in your component code. This way you never have to unsubscribe in an OnDestroy either...

    For example (in you onInit):

    this.distinctMovies$ = this.movieService.search(searchParams).pipe(distinctUntilChanged())
    

    And in the template just use an async pipe:

    *ngFor = "let movie of distinctMovies$ | async"
    

    Apart from not having to unsubscribe you can also use the OnPush ChangeDetectionStrategy by using async pipes instead of subscribing.