Стоит начать с боли всех разработчиков Angular: когда начинаешь свой проект, всё чисто и красиво. Но когда проект уже идёт, появляются подписки на подписки, данные из разных запросов нужно объединить, а пользователь начинает нажимать кнопки слишком быстро.
И здесь приходит на помощь RxJS
RxJS часто пугает своей сложностью. Прикол в том, что вам не нужно знать все 100+ операторов. Достаточно освоить базовую пятёрку, которая покроет 80% проблем. И после того, как освоишь их, код становится намного читабельнее и быстрее.
switchMap: для поисковых строк и быстрых кликов
Когда пользователь набирает текст в строку поиска, каждый его символ может отправлять запрос на сервер. Если он набирает быстро, то таких запросов будет много. Сервер получает лишнюю нагрузку, а ответы могут прийти не по порядку: ответ на первый символ может прийти позже ответа на второй. Также подсказки будут показываться неправильно.
Оператор switchMap решает эту проблему. При появлении нового значения в потоке, switchMap отписывается от предыдущего внутреннего потока и начинает новый.
Таким образом, в обработчик будет передаваться только ответ самого нового запроса.
В Angular это делается следующим образом:
this.searchControl.valueChanges.pipe( switchMap(query => this.http.get(`/api/search?q=${query}`)) ).subscribe(results => { this.searchResults = results; });
Берём поток изменений значения контрола valueChanges. Внутри switchMap делаем HTTP-запрос с помощью HttpClient.
SwitchMap гарантирует, что subscribe получит ответ только по последнему запросу. Если пользователь набрал ang и быстро добавил ular, то запрос на ang будет отменён (браузер прервёт соединение), и данные придут только по финальному запросу angular.
SwitchMap можно использовать и в других местах, когда нужно получить только ответ по последнему действию пользователя, например, при поиске, при автозаполнении, при переключении вкладок.
debounceTime: чтобы не дёргать сервер по каждой букве
Делать запрос на сервер после каждого введённого пользователем символа — бред. Пользователь набирает слово из десяти букв и десять раз стучится к серверу. Это лишняя работа, особенно если запросы тяжёлые, и интернет медленный.
В этом помогает оператор debounceTime. Он пропускает запросы при отсутствии событий. Для этого необходимо задать время. Пусть это будет 300 миллисекунд. Пользователь набирает что-то, и при этом клиент ждёт 300 миллисекунд перед отправкой запроса на сервер.
Если за это время не пришло никаких событий, то запрос идёт дальше. Но если пользователь продолжает набирать, то таймер сбрасывается, и начинается всё сначала.
В коде это выглядит так. Мы поставили debounceTime перед switchMap, и теперь запрос идёт после некоторой паузы.
this.searchControl.valueChanges.pipe( debounceTime(300), switchMap(query => this.http.get(`/api/search?q=${query}`)) ).subscribe(results => { this.searchResults = results; });
Допустим, пользователь набирает pivo_po_skidke и делает это быстро. Без debounceTime будет семь запросов p, pi, piv, pivo ...и так далее... до полного сообщения pivo_po_skidke. С debounceTime после p таймер начнёт работать, но сразу приходит i, и таймер сбрасывается. И так до конца. Когда пользователь остановился, таймер досчитал до 300 миллисекунд и пошёл с уже готовым запросом pivo_po_skidke.
Время задержки выбираем в зависимости от конкретного сценария. Для поиска достаточно 300-500 миллисекунд. Если сделать меньше, то будет сглаживаться действие debounceTime, если сделать больше, то пользователь будет ждать подсказку слишком долго.
forkJoin: когда нужно подождать все запросы
Нередко возникает ситуация, когда необходимо показать данные из нескольких источников. Допустим, мы открываем профиль пользователя и видим основную информацию о нём, список заказов и уведомления. Для каждого раздела необходимо сделать запрос и получить соответствующую информацию. Простое, но топорное решение сделать три независимых запроса и в каждом запросе передавать соответствующую часть страницы. Но тогда интерфейс будет грузиться частями. Сначала грузится одно поле, потом другое. Иногда необходимо показать всё сразу, и тогда пригодится forkJoin.
Он принимает массив или объект с Observable, ждёт, пока не завершится каждый из них, и возвращает массив (или объект) с последними значениями. Оператор forkJoin работает только в том случае, если все переданные потоки завершатся, то есть если каждый из переданных ему Observable отправит значение и завершится. В случае с HTTP-запросами из HttpClient это идеальный вариант, потому что каждый запрос отправляет значение и закрывается.
Очень удобно использовать forkJoin, потому что можно легко обращаться к результатам по ключам. Если бы мы передали массив, то получили бы массив результатов.
import { forkJoin } from 'rxjs'; forkJoin({ user: this.userService.getProfile(), orders: this.orderService.getOrders(), notifications: this.notificationService.getUnread() }).subscribe({ next: (results) => { this.user = results.user; this.orders = results.orders; this.notifications = results.notifications; }, error: (err) => { console.error('Не удалось загрузить данные', err); } });
Если хотя бы один из потоков завершится ошибкой, forkJoin сразу вызовет error, и результаты остальных потоков будут утеряны. Иногда это нормально, но если нужно получить хоть что-то, лучше обрабатывать ошибки отдельно для каждого потока.
combineLatest: для сложных форм и фильтров
Представьте, что у нас есть страница, на которой есть несколько фильтров: страна, город, категория товаров. И когда пользователь выбирает значение для каждого из этих фильтров, нужно сразу показывать обновлённые данные.
В частности, в Angular этот оператор часто используется с формами. Итак, у нас есть FormGroup с тремя контролами, и мы можем получить потоки значений этих контролов с помощью valueChanges и передать их в combineLatest.
import { combineLatest } from 'rxjs'; const country$ = this.filterForm.get('country').valueChanges.pipe( startWith(this.filterForm.get('country').value) ); const city$ = this.filterForm.get('city').valueChanges.pipe( startWith(this.filterForm.get('city').value) ); const category$ = this.filterForm.get('category').valueChanges.pipe( startWith(this.filterForm.get('category').value) ); combineLatest([country$, city$, category$]) .pipe( debounceTime(300), switchMap(([country, city, category]) => this.productService.getProducts({ country, city, category }) ) ) .subscribe(products => { this.products = products; });
Теперь, когда пользователь меняет страну, combineLatest получит новое значение страны вместе с последними актуальными значениями города и категории. Благодаря оператору startWith каждый поток при подписке сразу выдаёт текущее значение соответствующего контрола. Это гарантирует, что combineLatest сработает немедленно при любом изменении, используя все доступные данные формы.
Кстати, что combineLatest срабатывает очень часто — при каждом изменении любого из потоков. Итак, лучше положить debounceTime после него, иначе будет слишком много запросов, особенно если пользователь будет быстро переключать фильтры.
catchError: чтобы приложение не падало
В реальных приложениях может случиться так, что сервер ляжет, интернет отвалится, а сервер вернёт 500-ю ошибку. Если просто подписаться на этот запрос и ничего с ним не сделать, то Angular выбросит исключение в консоль и отменит подписку. А иногда и вовсе часть интерфейса просто перестанет работать, и пользователь увидит белый экран.
Оператор catchError ловит исключение из потока и даёт возможность вернуть новый поток или выбросить исключение выше. Чаще всего внутри catchError показывают сообщение пользователю и возвращают какой-то поток по умолчанию (например, пустой массив), чтобы поток не прерывался, и интерфейс работал дальше.
В Angular это удобно делать либо прямо в сервисе, либо в компоненте.
import { catchError } from 'rxjs'; import { of } from 'rxjs'; this.http.get('/api/products').pipe( catchError(error => { console.error('Ошибка при загрузке товаров', error); this.notificationService.show('Не удалось загрузить товары'); return of([]) }) ).subscribe(products => { this.products = products; });
Без catchError поток просто закончится, и метод subscribe уже не будет получать никаких значений. А с catchError мы вернём новый поток с пустым массивом, и метод subscribe получит значение из этого потока. Поток закончится нормально (потому что мы вернули of).
Иногда могут потребоваться какие-то действия при ошибке, а затем снова завершить поток. В этом случае можно использовать EMPTY из RxJS, который является пустым observable, завершаемым сразу без выдачи каких-либо значений. Важно, чтобы интерфейс не развалился и программа продолжила работу.
Заключение
Пять операторов, о которых мы говорили, обрабатывают самые частые сценарии при работе с асинхронными данными в Angular. Например, switchMap отменяет устаревшие запросы, debounceTime снижает нагрузку, forkJoin объединяет параллельные запросы, а combineLatest помогает следить за группой источников данных, catchError делает приложение устойчивым к ошибкам.
© 2026 ООО «МТ ФИНАНС»

