На протяжении многих лет RxJS был основой реактивности в Angular. Однако одним из его главных недостатков с точки зрения синхронизации данных и представлений является его статичность. С концептуальной точки зрения, функции observables — это скорее про обработку событий, чем про управление данными. По этой причине команда Angular представила новый примитив — сигналы. Но как нам теперь относиться к RxJS?
Основная концепция observables заключается в использовании уведомлений. Потоки и их операторы отлично подходят для описания сложной событийно-управляемой логики, особенно при работе с императивными API в браузерной среде. С учетом того, что в Angular появились инструменты, обеспечивающие гладкое взаимодействие между сигналами и функциями observables, мы получили в свое распоряжение невероятно мощную синергию.
В этой статье мы покажем примеры кода, в которых эти две концепции дополняют друг друга.
Будем следовать простому правилу
Больше никаких observables в шаблонах. До свидания, async!
До появления сигналов реактивность в Angular основывалась на подписке на observables в шаблонах с помощью AsyncPipe. AsyncPipe был разработан для решения двух ключевых задач: пометки представления как «нечистого» для обнаружения изменений (это необходимо при использовании стратегии OnPush) и привязки к жизненному циклу компонента для отмены подписки на observable после уничтожения представления.
Однако это не решило фундаментальную проблему, о которой я говорил ранее: observables не гарантируют состояние после подписки, что привело к тому, что Angular стал выводить возвращаемое значение конвейера в виде объединения с null. Это вынуждало разработчиков добавлять дополнительные проверки, что оказалось довольно раздражающим.

Настало время полностью отказаться от использования observables в шаблонах и перейти к сигналам, которые отслеживают состояние и обладают дополнительными преимуществами, которые мы рассмотрим в примерах.
Предупреждение: в примерах кода используется API RxJS Interop, который пока находится в стадии предварительного рассмотрения разработчиками.
Кнопка копирования (copy)
Решим простую задачу: при нажатии на кнопку копирования нам нужно обеспечить обратную связь с пользователем, изменив иконку и текст на пару секунд, а затем вернув их в исходное состояние:
readonly copied = toSignal(
fromEvent(inject(ElementRef).nativeElement, 'click').pipe(
exhaustMap(() =>
timer(2000).pipe(
map(() => false),
startWith(true)
)
)
),
{ initialValue: false }
);
Результат (stackblitz):

Используя преимущества операторов RxJS, мы декларативно описали, как изменяется состояние copied:
- Если пользователь нажимает на кнопку несколько раз,
exhaustMapгарантирует завершение текущегоtimerдолжным образом, прежде чем состояние сбросится вfalse.
- Логика синхронизации выделяемых значений с сигналом и отписки от исходной observable обрабатывается toSignal, что устраняет необходимость в ручных подписках.
В предыдущем разделе мы говорили о нецелесообразности использования AsyncPipe. Укажем в связи с этим, что для потоков можно использовать новую альтернативу — toSignal.
Подсветка анкоров
Перейдем к более сложной задаче. Наша страница содержит ссылки-анкоры, которые должны подсвечиваться, когда пользователь переходит по URL-адресу с соответствующим фрагментом. Поскольку во время навигации раздел, содержащий ссылку, может оказаться за пределами области просмотра, подсветка должна быть применена только после завершения автоматической прокрутки:
private readonly id = inject(new HostAttributeToken('id'));
private readonly route = inject(ActivatedRoute);
readonly highlighted = toSignal(
this.route.fragment.pipe(
startWith(this.route.snapshot.fragment),
filter(fragment => this.id === fragment),
switchMap(() =>
concat(
fromEvent(window, 'scroll').pipe(
startWith(true),
debounceTime(100),
take(1)
),
timer(2000).pipe(map(() => false))
)
)
),
{ initialValue: false }
);
Кстати, еще одним недостатком observables при управлении состоянием была невозможность привязать данные непосредственно к хост-элементу. Сигналы решают эту проблему, и с их помощью мы можем это сделать:
host: {
'[class.highlighted]': 'highlighted()'
}
Результат (stackblitz):

RxJS в очередной раз помог нам избежать каскада императивного кода:
debounceTimeвместе сtakeпозволяет нам дождаться окончания автоматической прокрутки, прежде чем обновить состояние доtrue.
timerсбрасывает состояние до начального значения.
- Порядок подписок управляется
concat, который подписывается на таймер сброса только после завершения работы первой observable.
Автоматическое отключение
Решим задачу реализации компонента уведомления с автоматическим отключением. В качестве входных данных принимается длительность отображения и выдается событие, когда оно должно закрыться. Кроме того, есть еще одно требование: если пользователь наведет курсор на уведомление, таймер должен быть остановлен и сброшен; таким образом предотвращается отключение до тех пор, пока курсор не покинет элемент:
private readonly el = inject(ElementRef).nativeElement;
readonly duration = input(Infinity);
readonly close = outputFromObservable(
toObservable(this.duration).pipe(
switchMap(value => Number.isFinite(value) ? timer(value) : EMPTY),
takeUntil(fromEvent(this.el, 'mouseenter')),
repeat({ delay: () => fromEvent(this.el, 'mouseleave') })
)
);
Результат (stackblitz):

В этой задаче мы использовали две вспомогательные функции для работы с обновленными API input и output в Angular.
toObservable преобразует вход на основе сигнала в поток. Angular проделал отличную работу по улучшению своей системы реактивности, сделав входные свойства директив и компонентов по-настоящему реактивными. Теперь эти свойства можно использовать как в сценариях управления состоянием (например, применяя вычисляемые свойства), так и для создания логики, основанной на взаимодействии с observables.
Важно отметить, что сигналы не испытывают сбоев и никогда не распространяют изменения синхронно. Если значение сигнала обновляется синхронно несколько раз, подписчик получит уведомление о последнем значении только после того, как сигнал стабилизируется в процессе обнаружения изменений. Это важно иметь в виду, если вы планируете строить реактивные цепочки с использованием toObservable.
outputFromObservable создает новый вывод из observable. С введением новых входов Angular пришлось отказаться от EventEmitter, который ранее был расширен за счет Subject из RxJS. Хотя здесь и нет ничего принципиально нового, стоит отметить, что «под капотом» вывод теперь использует экземпляр нового класса OutputEmitterRef. Этот класс следует той же ментальной модели, что и Subject, но в более легковесном варианте.
Отслеживание размеров элемента
Когда речь заходит об API в браузерной среде, бывает неудобно использовать их в декларативном виде. Большинство таких API основаны на обратных вызовах, а некоторые также включают логику очистки, подобную той, что известна специалистам по RxJS под названием TeardownLogic.
Однако, хотя RxJS и предоставляет унифицированный механизм для построения реактивных цепочек, работа с императивными API часто требует написания более многословных и последовательных инструкций.
Поскольку сигналы легко интегрируются с observables, с императивными API можно обращаться как с потоками. Определим для примера пользовательский оператор для превращения ResizeObserver в observable:
export function fromResizeObserver(
target: Element | SVGElement,
options?: ResizeObserverOptions,
): Observable<ResizeObserverEntry[]> {
return new Observable(subscriber => {
const ro = new ResizeObserver(entries => subscriber.next(entries));
ro.observe(target, options);
return () => ro.disconnect();
});
}
Этот оператор можно повторно использовать во всей кодовой базе. Например, мы можем прибегнуть к нему при создании реактивного состояния для параметра width элемента хоста:
private readonly el = inject(ElementRef).nativeElement;
readonly width = toSignal(
fromResizeObserver(this.el).pipe(map(() => this.el.offsetWidth)),
{ initialValue: 0 }
);
Для точного отслеживания ширины элемента часто требуется не только ResizeObserver. В пограничных случаях могут понадобиться дополнительные наблюдатели, такие как MutationObserver. Здесь RxJS демонстрирует свою истинную силу: операторы слияния позволяют декларативно объединять события для надежного обнаружения изменений ширины элемента:
private readonly el = inject(ElementRef).nativeElement;
readonly width = toSignal(
merge(
fromResizeObserver(this.el),
fromMutationObserver(this.el, {
childList: true,
subtree: true,
characterData: true,
})
// ...а также другие пограничные случаи
).pipe(
map(() => this.el.offsetWidth),
distinctUntilChanged()
),
{ initialValue: 0 }
);
Повышение эффективности API Angular
В предыдущем примере мы создали пользовательский оператор, чтобы обернуть API, специфичный для браузера. Аналогичный подход можно применить для построения реактивных взаимодействий с некоторыми API Angular, создав на их основе функции observables.
В некоторых предыдущих фрагментах кода я использовал глобальные объекты, доступные только в среде браузера (например, window). Однако, чтобы гарантировать правильное выполнение кода на сервере (SSG/SSR), нам нужно убедиться, что определенная логика инициализируется только во время рендеринга на стороне клиента. В таких случаях мы можем использовать API afterNextRender (все еще на стадии рассмотрения разработчиками), который основан на обратном вызове, и превратить его в observable:
export function fromAfterNextRender(options?: AfterRenderOptions): Observable<void> {
if (!options?.injector) {
assertInInjectionContext(fromAfterNextRender);
}
return new Observable(subscriber => {
const ref = afterNextRender(() => {
subscriber.next();
subscriber.complete();
}, options);
return () => ref.destroy();
});
}
Затем можно использовать его для вычисления состояния исключительно на стороне клиента:
readonly state = toSignal(
fromAfterNextRender().pipe(
switchMap(() => {
// здесь мы находимся в контексте браузера
}),
),
);
Еще один пример — создание пользовательского оператора на основе частой последовательности операций для сокращения количества шаблонного кода и улучшения читабельности.
В одном из моих проектов было много прикладной логики, привязанной к событиям Router, поэтому вместо того, чтобы писать ее следующим образом:
private readonly router = inject(Router);
constructor() {
this.events.subscribe(e => {
if (e instanceof NavigationEnd) {
// Сужение типа `e` доступно только в этом блоке
}
})
}
Гораздо удобнее писать вот так:
/*
Теперь мы можем использовать наблюдаемую переменную с типизированным значением соответствующего события любым удобным для нас способом
Returns Observable<NavigationEnd>
*/
fromRouterEvent(NavigationEnd);
Для этого достаточно написать обычный оператор, используя существующий оператор filter и предикат типа для обеспечения сужения типа:
import { Event, Router } from '@angular/router';
export function fromRouterEvent<T extends Event>(
event: { new (...args: any[]): T },
options?: { injector: Injector },
): Observable<T> {
let router: Router;
if (!options?.injector) {
assertInInjectionContext(fromRouterEvent);
router = inject(Router);
} else {
router = options.injector.get(Router);
}
return router.events.pipe(filter((e: unknown): e is T => e instanceof event));
}
Другими словами, у нас есть возможность адаптировать необходимые API и частые операции фреймворка для работы с потоками, значительно сократив шаблонный код и сделав его более декларативным.
Заключение
Описанные выше кейсы представляют собой практические примеры из реальных проектов. Как видно из приведенных решений, RxJS отлично подходит для выполнения событийно-управляемых задач. Вдобавок ко всему, реактивная библиотека остается интегрированной в экосистему Angular на уровне публичных API (например, в @angular/{router,forms,сommon/http,cdk}).
Синергия между сигналами и observables может стать важным шагом вперед в развитии реактивности Angular, предлагая более структурированный подход, при котором сигналы фокусируются на управлении состоянием, а observables обрабатывают события.
Читайте также:
- Как дуэт Angular-Wiz поменяет правила игры
- Angular: наведение мостов между HttpClient и Signals
- Angular-приложения универсальной сборки
Читайте нас в Telegram, VK и Дзен
Перевод статьи Vyacheslav Borodin: RxSignals: The most powerful synergy in the history of Angular





