Angular
RxJS-интероп: toSignal и toObservable
В Angular годами всё реактивное держалось на RxJS: HTTP-запросы возвращали Observable, события форм текли потоками, маршрутизатор отдавал параметры как поток. С приходом сигналов появился второй реактивный мир, заточенный под состояние. Возникает вопрос: переписывать ли весь RxJS на сигналы? Ответ - нет. У каждого инструмента своя сильная сторона, и Angular даёт мост между ними: toSignal превращает поток в сигнал для удобного чтения в шаблоне, toObservable превращает сигнал в поток для богатых операторов RxJS.
- HTTP-запросы: HttpClient возвращает Observable, а toSignal делает результат удобным для шаблона
- Параметры маршрута: ActivatedRoute отдаёт потоки, которые превращают в сигналы для реактивного UI
- Поиск с задержкой: поток ввода с debounceTime и switchMap - классическая зона RxJS
- WebSocket и серверные события: непрерывные потоки данных естественно выражаются в RxJS
- Очистка подписок: takeUntilDestroyed автоматически отписывается при удалении компонента
Предварительные знания
- Понимание сигналов: signal, computed, чтение через вызов
- Базовое знакомство с Observable: подписка, поток значений во времени
- Идея об операторах RxJS (map, filter, debounceTime) на уровне назначения
Два реактивных мира в одном фреймворке
RxJS был частью Angular с самого начала второй версии (2016): HttpClient, формы, маршрутизатор - всё построено на Observable. Это давало мощь, но и порог входа: операторы, подписки, ручная отписка. Когда в 2023 году появились сигналы, у фреймворка стало два реактивных подхода. Команда Angular сразу прояснила позицию: сигналы не заменяют RxJS, а дополняют его. Сигналы хороши для синхронного состояния и шаблонов, RxJS - для потоков событий и сложной асинхронности. Чтобы связать миры, в пакет @angular/core/rxjs-interop вошли toSignal и toObservable, а также takeUntilDestroyed для автоматической отписки. К Angular 21 эта пара стала стандартным мостом между двумя подходами.
toSignal: поток как сигнал
toSignal принимает Observable и возвращает сигнал, который всегда содержит последнее излучённое потоком значение. Подписка создаётся автоматически, и так же автоматически закрывается, когда исчезает контекст инъекции, в котором был вызван toSignal. Это убирает классическую боль RxJS - ручную подписку и отписку в шаблоне или хуках.
Параметр initialValue задаёт значение сигнала до того, как поток излучит первое. Без него сигнал может быть undefined до прихода данных, что нужно учитывать в типе. С initialValue тип становится чище, а шаблон не приходится защищать от undefined.
После превращения в сигнал результат читают в шаблоне просто как users(), без асинхронного пайпа и без ручной подписки. Сигнал интегрируется с computed и effect, поэтому данные из сети дальше комбинируются с остальным состоянием обычными сигнальными средствами.
Что делает toSignal с переданным Observable?
toObservable: сигнал как поток
Обратное направление нужно, когда над значением сигнала хочется применить операторы RxJS. toObservable принимает сигнал и возвращает Observable, который излучает новое значение при каждом изменении сигнала. Это открывает доступ к богатой библиотеке операторов: debounceTime для задержки, switchMap для отмены устаревших запросов, distinctUntilChanged для фильтрации повторов.
Здесь видна сила связки. Состояние ввода - это сигнал query, удобный для двусторонней привязки. Но логика поиска (подождать паузу в наборе, отменить предыдущий запрос при новом вводе) естественно выражается операторами RxJS. toObservable отдаёт поток из query, операторы делают работу, а toSignal возвращает результат обратно в удобный для шаблона сигнал.
toObservable работает в контексте инъекции и использует effect под капотом, чтобы следить за сигналом. Поэтому его, как и toSignal, вызывают в поле или конструкторе компонента или сервиса, а не внутри произвольного метода.
Зачем превращать сигнал в Observable через toObservable?
Когда RxJS, когда сигналы, и takeUntilDestroyed
Граница между подходами проходит по природе задачи. Сигналы созданы для синхронного состояния: текущее значение, которое читают в шаблоне и комбинируют через computed. RxJS создан для потоков событий и сложной асинхронности: последовательности значений во времени, отмена, объединение нескольких источников, тонкий контроль времени. Хорошее приложение использует оба, выбирая по задаче.
| Задача | Лучше подходит | Почему |
|---|---|---|
| Текущее значение для шаблона | Сигнал | Синхронное чтение, интеграция с computed |
| Производное состояние | Сигнал (computed) | Автоотслеживание зависимостей, мемоизация |
| Поиск с задержкой набора | RxJS | debounceTime и switchMap отменяют устаревшие запросы |
| WebSocket, серверные события | RxJS | Непрерывный поток значений во времени |
| Объединение нескольких потоков | RxJS | Операторы combineLatest, merge, forkJoin |
Когда RxJS всё же используется напрямую с ручной подпиской, остаётся вопрос отписки. Оператор takeUntilDestroyed автоматически завершает подписку при удалении компонента или сервиса, в контексте которого он вызван. Это убирает необходимость хранить Subscription и вручную отписываться в ngOnDestroy.
Простое правило выбора: если задача про текущее значение состояния - сигнал. Если про последовательность событий во времени или сложную асинхронность - RxJS. А toSignal и toObservable связывают их там, где удобнее объединить сильные стороны обоих.
Для какой из задач RxJS подходит лучше, чем сигналы?
Связь с другими темами
Интероп соединяет сигнальную модель курса с давно существующим миром RxJS:
- Сигналы — toSignal даёт сигнал из потока, toObservable - поток из сигнала
- computed — Сигнал, полученный из потока через toSignal, читается в computed наравне с обычным
- Сервисы и состояние на сигналах — Сервисы соединяют RxJS-загрузку с сигнальным состоянием через интероп
Итог
- toSignal подписывается на Observable и отдаёт его последнее значение в виде сигнала, отписываясь автоматически
- toObservable превращает сигнал в Observable, давая доступ к операторам RxJS
- RxJS уместен для потоков событий и сложной асинхронности (debounce, switchMap, объединение потоков)
- Сигналы уместны для синхронного состояния, которое читают в шаблоне и комбинируют через computed
- takeUntilDestroyed автоматически завершает подписку при удалении компонента, убирая ручной ngOnDestroy
Связанные уроки
- ng-11-signals-intro — Интероп строится на понимании сигналов как способа хранить и читать состояние
- ng-12-computed — Полученный через toSignal сигнал используется в computed как любой другой источник
- ng-18-services — Сервисы часто соединяют RxJS-загрузку данных с сигнальным состоянием через toSignal