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)Автоотслеживание зависимостей, мемоизация
Поиск с задержкой набораRxJSdebounceTime и 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
RxJS-интероп: toSignal и toObservable

0

1

Войти