Подписка RxJS в компоненте Angular

Я не совсем понимаю, где и как мне нужно объявлять наблюдаемые / объекты в компоненте Angular.

В настоящее время я разрабатываю веб-сайт, который взаимодействует с MovieDB API, и у меня все работает, но в то же время я понимаю, что мой код плохой, потому что после уничтожения компонента нет очистки подписок, но для выполнения этой очистки мне нужно на хотя бы понять, как правильно использовать RxJS.

Я думаю, что мое использование неверно, потому что у меня есть новая подписка на каждое взаимодействие со страницей. Также, насколько я понимаю, их нужно объявить в конструкторе.

Идея этой страницы заключается в том, что есть форма ввода, в которой пользователь вводит запрос и устанавливает переключатель, чтобы выбрать, что искать: «телевизор» или «фильмы». Когда есть результаты поиска, появляется кнопка для раскрытия результатов.

Итак, вот код:

      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();
  }
}

И 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>

Буду очень признателен, если вы мне поможете и подскажете, где я ошибался. В дальнейшем все работает именно так, но я хочу разобраться в использовании RxJS.

2 ответа

Похоже, вы понимаете основы.

Хотя в конструкторе ничего объявлять не нужно.

Также, насколько я понимаю, их нужно объявить в конструкторе.

Обычно вы используете конструктор только для внедрения сервисов, как вы это уже делаете.

Возможно, вы захотите использовать метод ngOnInit() для объявления вашего начального состояния компонента:

      export class SearchTabComponent implements OnInit {
  distinctMovies$!: Observable<Movie[]>

  ngOninit(): void {
    //...
  }
}

Вы можете решить множество проблем, просто никогда не подписываясь на код компонента. Таким образом, вам никогда не придется отказываться от подписки в OnDestroy ...

Например (у вас onInit):

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

А в шаблоне просто используйте асинхронный канал:

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

Помимо того, что вам не нужно отказываться от подписки, вы также можете использовать OnPush ChangeDetectionStrategy, используя асинхронные каналы вместо подписки.

Глядя на первый взгляд, вы можете здесь кое-что улучшить.

По поводу отказа от подписки

Для этого есть несколько шаблонов, которые считаются "хорошей практикой". Один из них является:

      // Define destroy subject
readonly destroy$ = new Subject<any>();

// On each subscription add
.pipe(takeUntil(this.destroy$))

// And just emit value to destroy$ subject in onDestroy hook
// And all streams that had takeUntil will be ended
ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

Что касается поиска вещей

Когда вы имеете дело с асинхронными запросами (такими как вызовы API), вы должны думать, что произойдет, если, например, вы несколько раз нажмете кнопку «Поиск». Будет несколько вызовов API, верно? но мы не уверены на 100%, каков будет порядок ответов от сервера.

Если вы, например, нажмете:

Click 1, Click 2, Click 3, Click 4 ... создаст APICall1, APICall2, APICall3, APICALL4 ... это будет в таком порядке ... Но ответы от сервера могут быть в другом порядке (да, это возможно ), также если вы нажмете несколько раз без задержки, ваш сервер будет получать много запросов в данный момент, но вам, вероятно, понадобится только последний.

Таким образом, одним из распространенных решений является наличие потока, который прослушивает, например, изменения поиска:

      searchTerm = new Subject();

ngOnInit() {

  this.searchTerms
    .pipe(
      distinctUntilChanged(),
      // Debounce for 250 miliseconds (avoid flooding backend server with too many requests in a short period of time)
      debounceTime(250),
      // Call api calls
      switchMap(terms => {
        return this.movieService.serach(terms);
      })),
      // Unsubsribe when onDestroy is triggered
      takeUntil(this.destroy$),
    .subscribe(results => {
         // Push/set results to this.searchResults
         if (this.addingMore) {
         //  push in this.searchResults
         } else {
            this.searchResults = results;
         }

         this.addingMore = false;
    });

}

search(searchTerms) {
  this.searchParams = searchParams;
  this.searchTerms.next(searchTerms);
}

showMore(): void {
  if (this.isFinished() return;

  this.searchParams.page++;

  // You can for example set a flag to know if its search or loadMore
  this.addingMore = true;
 
  this.searchTerms.next(searchTerms);
}
Другие вопросы по тегам