Я работал над SPA с Angular 16, TypeScript и The Movie Database (TMDB).
Я столкнулся с проблемой после разработки функции «бесконечная прокрутка».
В компоненте MoviesByGenre у меня есть:
import { Component } from '@angular/core';
import { GenreResponse, Genre } from '../../models/Genre';
import { MovieResponse, Movie } from '../../models/Movie';
import { MovieService } from '../../services/movie-service.service';
import { ActivatedRoute } from '@angular/router';
import { distinctUntilChanged, fromEvent, map, startWith } from 'rxjs';
@Component({
selector: 'app-movies-by-genre',
templateUrl: './movies-by-genre.component.html',
styleUrls: ['./movies-by-genre.component.scss']
})
export class MoviesByGenre {
constructor(
private activatedRoute: ActivatedRoute,
private movieService: MovieService
) { }
public genreName: string | undefined = '';
public movieResponse!: MovieResponse;
public movies: Movie[] = [];
public genreResponse!: GenreResponse;
public genres: Genre[] | undefined = [];
public genreId!: number;
public maxPage: number = 10;
public pageNumber: number = 1;
public isLoading: boolean = false;
public getMoviesByGenre(): void {
// Get genre id (from URL parameter)
this.genreId = Number(this.activatedRoute.snapshot.paramMap.get('id'));
// Get genre name from genres array
this.movieService.getAllMovieGenres().subscribe((response) => {
this.genreResponse = response;
this.genres = this.genreResponse.genres;
if (this.genres && this.genres.length) {
let currentGenre = this.genres.find(
(genre) => genre.id === this.genreId
);
if (currentGenre) {
this.genreName = currentGenre.name || '';
this.movieService.defaultTitle = this.genreName;
}
}
});
this.loadMoreMovies(this.genreId, this.pageNumber);
}
public loadMoreMovies(genreId: number, pageNumber: number) {
// Get movies by genre id
this.movieService
.getMoviesByGenre(genreId, pageNumber)
.subscribe((response) => {
this.movieResponse = response;
this.movies.push(...(this.movieResponse?.results || []));
});
}
ngAfterViewInit() {
fromEvent(window, 'scroll')
.pipe(
startWith(0),
map(() => window?.scrollY),
distinctUntilChanged()
)
.subscribe((scrollPos: any) => {
if (!this.movies?.length) {
return;
}
if (
Math.round(scrollPos + window.innerHeight) >=
document.documentElement.scrollHeight &&
this.pageNumber < this.maxPage
) {
this.pageNumber++;
this.loadMoreMovies(this.genreId, this.pageNumber);
}
});
}
ngOnInit() {
this.activatedRoute.params.subscribe(() => {
this.movies = [];
this.getMoviesByGenre();
});
}
ngOnDestroy() {
this.movieService.defaultTitle = '';
}
}
В сервисе MovieService:
import { environment } from '../../environments/environment';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { MovieResponse, Movie } from '../models/Movie';
import { GenreResponse } from '../models/Genre';
import { TrailerResponse } from '../models/Trailer';
@Injectable({
providedIn: 'root'
})
export class MovieService {
constructor(private http: HttpClient) { }
public defaultTitle: string = '';
public getAllMovieGenres(): Observable<GenreResponse> {
return this.http.get<GenreResponse>(`${environment.apiUrl}/genre/movie/list?api_key=${environment.apiKey}`);
}
public getMoviesByGenre(id: Number, pageNumber: Number): Observable<MovieResponse> {
return this.http.get<MovieResponse>(`${environment.apiUrl}/discover/movie?api_key=${environment.apiKey}&with_genres=${id}&page=${pageNumber}`);
}
public getMovieDetails(id: Number): Observable<Movie>{
return this.http.get<Movie>(`${environment.apiUrl}/movie/${id}?api_key=${environment.apiKey}`);
}
}
При каждой прокрутке страницы вниз по маршруту by-genre/:id загружается еще 20 фильмов с ограничением в 10 страниц (200 фильмов).
Проблема в том, что даже на других маршрутах, таких как маршрут с подробностями фильма, при прокрутке страницы вниз выполняется вызов API, который показывает счетчик загрузки, для которого я без необходимости использовал технику перехватчика http.
Цель состоит в том, чтобы сделать бесконечную прокрутку специфичной для компонента MoviesByGenre:
Для этой цели я добавил логическое значение в компонент MoviesByGenre и использовал его следующим образом:
public isLoadMore: boolean = true;
if (this.isLoadMore) {
this.loadMoreMovies(this.genreId, this.pageNumber);
}
Но запрос по-прежнему выполняется для других компонентов при прокрутке страницы вниз.
Похоже, мне не удалось ни определить причину проблемы, ни предложить жизнеспособное решение.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Чтобы решить эту проблему, нам нужно unsubscribe перейти к активным подпискам. Когда компонент уничтожается, это не обязательно означает, что подписки уничтожаются. Нам нужно add их всех subscription и вызвать unsubscribe метод ngOnDestroy компонента, чтобы уничтожить все подписки.
Рекомендуется добавлять все подписки в свойство подписки, а затем полностью от них отказываться.
import { Component } from '@angular/core';
import { GenreResponse, Genre } from '../../models/Genre';
import { MovieResponse, Movie } from '../../models/Movie';
import { MovieService } from '../../services/movie-service.service';
import { ActivatedRoute } from '@angular/router';
import {
Subscription,
distinctUntilChanged,
fromEvent,
map,
startWith,
} from 'rxjs';
@Component({
selector: 'app-movies-by-genre',
templateUrl: './movies-by-genre.component.html',
styleUrls: ['./movies-by-genre.component.scss'],
})
export class MoviesByGenre {
private subscription = new Subscription(); // <- changed here!
constructor(
private activatedRoute: ActivatedRoute,
private movieService: MovieService
) {}
public genreName: string | undefined = '';
public movieResponse!: MovieResponse;
public movies: Movie[] = [];
public genreResponse!: GenreResponse;
public genres: Genre[] | undefined = [];
public genreId!: number;
public maxPage: number = 10;
public pageNumber: number = 1;
public isLoading: boolean = false;
public getMoviesByGenre(): void {
// Get genre id (from URL parameter)
this.genreId = Number(this.activatedRoute.snapshot.paramMap.get('id'));
// Get genre name from genres array
this.subscription.add( // <- changed here!
this.movieService.getAllMovieGenres().subscribe((response) => {
this.genreResponse = response;
this.genres = this.genreResponse.genres;
if (this.genres && this.genres.length) {
let currentGenre = this.genres.find(
(genre) => genre.id === this.genreId
);
if (currentGenre) {
this.genreName = currentGenre.name || '';
this.movieService.defaultTitle = this.genreName;
}
}
})
);
this.loadMoreMovies(this.genreId, this.pageNumber);
}
public loadMoreMovies(genreId: number, pageNumber: number) {
// Get movies by genre id
this.subscription.add( // <- changed here!
this.movieService
.getMoviesByGenre(genreId, pageNumber)
.subscribe((response) => {
this.movieResponse = response;
this.movies.push(...(this.movieResponse?.results || []));
})
);
}
ngAfterViewInit() {
this.subscription.add( // <- changed here!
fromEvent(window, 'scroll')
.pipe(
startWith(0),
map(() => window?.scrollY),
distinctUntilChanged()
)
.subscribe((scrollPos: any) => {
if (!this.movies?.length) {
return;
}
if (
Math.round(scrollPos + window.innerHeight) >=
document.documentElement.scrollHeight &&
this.pageNumber < this.maxPage
) {
this.pageNumber++;
this.loadMoreMovies(this.genreId, this.pageNumber);
}
})
);
}
ngOnInit() {
this.subscription.add( // <- changed here!
this.activatedRoute.params.subscribe(() => {
this.movies = [];
this.getMoviesByGenre();
})
);
}
ngOnDestroy() {
this.subscription.unsubscribe(); // <- changed here!
this.movieService.defaultTitle = '';
}
}