С выпуском Angular 18 команда команда разработчиков значительно расширила функциональность RxJS Interop, что упрощает интеграцию между Signals и RxJS Observables, оптимизируя производительность и улучшая читаемость кода. В этой статье мы рассмотрим новые возможности RxJS Interop, примеры их применения и объясним, как они помогают сделать ваш код чище и эффективнее.
Эволюция RxJS Interop в Angular
RxJS Interop впервые был представлен в Angular 16 для преодоления разрыва между Signals и RxJS Observables. Первая версия позволяла разработчикам конвертировать Signals в Observables и наоборот. В Angular 17 функциональность была улучшена, чтобы сделать преобразования более эффективными и обеспечить лучшую интеграцию операторов RxJS с Signals.
С выпуском Angular 18 функциональность RxJS Interop значительно расширилась, предлагая улучшенную поддержку операторов, более гибкие настройки и дополнительные возможности, что делает управление реактивным состоянием ещё более удобным.
Что такое RxJS Interop?
В Angular 16 RxJS впервые представил Signals с новым реактивным API. Этот API позволяет Angular точно определять, где происходит изменение. Теперь Angular может определять, какие компоненты необходимо обновить. Сигнал отмечает компонент как грязный, но не его предков. Когда Angular выполняет свой цикл обнаружения изменений сверху вниз, будет обновлен только этот конкретный компонент.
Также RxJS Interop позволяет разработчикам Angular легко комбинировать и конвертировать Signals и Observables. Традиционно Angular активно использовал RxJS для обработки асинхронных операций, таких как HTTP-запросы или пользовательские события. С появлением Signals Angular предлагает ещё один способ управления реактивным состоянием, который помогает упростить реактивные паттерны, а главное сделать Angular приложение более производительным.
Основные функции RxJS Interop
RxJS Interop позволяет:
Конвертировать Signals в Observables и наоборот.
Использовать операторы RxJS, такие как
map
,filter
иmerge
, с Signals.Упрощать работу с реактивными данными.
Использовать более гибкие настройки конверсии и управления состоянием.
Эти функции предоставляют разработчикам больше возможностей для управления реактивными паттернами, сохраняя при этом поддержку приложений на высоком уровне.
Краткий обзор RxJS в Angular
RxJS лежит в основе реактивной системы Angular, предоставляя тип Observable для управления асинхронными данными. Он используется во многих частях Angular, таких как HttpClient
, формы и обработка событий.
Observable: поток данных, который генерирует множество значений с течением времени.
Subject: Observable, который рассылает значения нескольким наблюдателям.
BehaviorSubject: хранит последнее значение и немедленно отправляет его новым подписчикам.
ReplaySubject: повторяет буфер предыдущих значений для новых подписчиков.
Эти паттерны мощные, но могут быть сложными при управлении состоянием или обработке нескольких асинхронных операций. Signals в сочетании с RxJS Interop помогают упростить такие сценарии.
RxJS Interop в Angular 18
Signals vs Observables: когда использовать что?
С Angular 18 разработчики получают больше гибкости в выборе между Signals и Observables:
Observables идеальны для непрерывных событийных данных, таких как HTTP-запросы или события форм.
Signals лучше подходят для реактивного состояния с предсказуемыми потоками данных.
RxJS Interop делает интеграцию между этими двумя системами бесшовной, повышая возможность повторного использования кода.
Основные функции RxJS Interop
Пример 1: Конвертация Signals в Observables
В этом примере мы создаем Signal и преобразуем его в Observable для использования в шаблоне:
<div>
<button (click)="incrementSignal()">Increment Signal</button>
<p>Observable Value: {{ myObservable$ | async }}</p>
<p>Signal Value: {{ mySignal() }}</p>
<p>Transformed Signal Value {{ squaredSignal() }}</p>
</div>
import { Component, computed, Signal, signal, WritableSignal } from '@angular/core';
import { Observable } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-first-example',
templateUrl: './first-example.component.html',
})
export class FirstExampleComponent {
mySignal: WritableSignal<number> = signal(0);
squaredSignal: Signal<number>
= computed(() => this.myDataTransformFn(this.mySignal()));
myObservable$: Observable<number>;
constructor() {
this.myObservable$ = toObservable(this.mySignal);
}
incrementSignal() {
this.mySignal.set(this.mySignal() + 1);
}
myDataTransformFn(value: number): number {
// You can implement any data transformation logic here
return value ** 2;
}
}
Этот пример демонстрирует несколько аспектов, таких как:
Конвертация Signal в Observable и их использование в шаблоне;
Формирование нового значения из Signal при помощи функции computed.
Хотелось бы отметить что Signal содержит в себе встроенный функционал кеширования (меморизации), что благотворно сказывается производительности вашего Angular приложения.
Пример 2: Использование Signals с дочерним компонентом
Этот пример демонстрирует передачу значений от дочернего компонента к родительскому с использованием Signals и Observables:
Родительский компонент:
<div>
<p>Child Observable Value In Parent as a Signal: {{ myChildOutputtedSignal() }}</p>
<p>Child Observable Value In Parent as an Observable {{ myChildOutputtedObservable$ | async }}</p>
<app-second-example-child></app-second-example-child>
</div>
import { AfterViewInit, Component, DestroyRef, inject, Signal, signal, viewChild, WritableSignal } from '@angular/core';
import { SecondExampleChildComponent } from './second-example-child/second-example-child.component';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
@Component({
selector: 'app-second-example',
templateUrl: './second-example.component.html',
})
export class SecondExampleComponent implements AfterViewInit {
destroyRef = inject(DestroyRef);
childComponent: Signal<SecondExampleChildComponent | undefined>
= viewChild<SecondExampleChildComponent>(SecondExampleChildComponent);
myChildOutputtedSignal: WritableSignal<number> = signal(0);
myChildOutputtedObservable$: Observable<number> = toObservable(this.myChildOutputtedSignal);
ngAfterViewInit() {
// No need to unsubscribe from this subscription on Destroy
this.childComponent()!.myObservableChange.subscribe((value: number) => {
// Save the value as you wish: BehaviourSubject, Signal, etc.
this.myChildOutputtedSignal.set(value);
});
// Better to unsubscribe from this subscription on Destroy Because the Observable
// is not guaranteed to complete when the component is destroyed
this.myChildOutputtedObservable$.pipe(
takeUntilDestroyed(this.destroyRef),
);
}
}
Дочерний компонент:
<div>
<p>Child Observable Value: {{ myObservableSubject$ | async }}</p>
<p>Child Signal From Observable Value: {{ mySignalFromObservable() }}</p>
<br>
<button (click)="incrementValue()">Increment</button>
</div>
import { Component, OutputRef } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { outputFromObservable, toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-second-example-child',
templateUrl: './second-example-child.component.html',
})
export class SecondExampleChildComponent {
myObservableSubject$ = new BehaviorSubject<number>(0);
myObservableChange: OutputRef<number> = outputFromObservable(this.myObservableSubject$);
// Some Observables are guaranteed to emit synchronously, such as BehaviorSubject.
// In those cases, you can specify the requireSync: true option
mySignalFromObservable = toSignal(this.myObservableSubject$, { requireSync: true });
incrementValue() {
this.myObservableSubject$.next(this.myObservableSubject$.value + 1);
}
}
Этот пример иллюстрирует работу таких функций:
Применение outputFromObservable - позволяет передавать потоки данных наружу в виде Observable.
Вызов toSignal - конвертирует любой Observable / Subject в Signal.
Добавление функции outputToObservable дает возможность подписаться на Output свойство дочернего компонента
Пример 3: Переход от старого подхода к новому
В этом примере показано, как переход от использования Observables к Signals может улучшить производительность и упростить код:
Родительский компонент:
<div>
<p>Old Approach Value: {{ oldApproachValue }}</p>
<p>New Approach Value: {{ newApproachValue }}</p>
<br>
<app-third-example-child
(incrementOldApproach)="incrementDefault()"
></app-third-example-child>
</div>
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { ThirdExampleChildComponent } from './third-example-child/third-example-child.component';
@Component({
selector: 'app-third-example',
templateUrl: './third-example.component.html',
})
export class ThirdExampleComponent implements AfterViewInit {
@ViewChild(ThirdExampleChildComponent) childComponent: ThirdExampleChildComponent | undefined;
oldApproachValue = 0;
newApproachValue = 0;
ngAfterViewInit() {
// No need to unsubscribe from this subscription on Destroy
this.childComponent?.incrementNewApproach.subscribe(() => {
this.newApproachValue++;
});
}
incrementDefault() {
this.oldApproachValue++;
}
}
Дочерний компонент:
<div>
<button (click)="increment()">increment</button>
</div>
import { Component, EventEmitter, output, Output } from '@angular/core';
@Component({
selector: 'app-third-example-child',
templateUrl: './third-example-child.component.html',
})
export class ThirdExampleChildComponent {
@Output() incrementOldApproach = new EventEmitter<void>();
// The output() function provides numerous benefits over decorator-based @Output and EventEmitter:
incrementNewApproach = output<void>();
increment() {
this.incrementOldApproach.emit();
this.incrementNewApproach.emit();
}
}
Функция output() обеспечивает более легкую читаемость кода и повышает производительность по сравнению с Output и EventEmitter на основе декораторов.
Пример 4: Сигналы с Firestore
В последнем примере мы используем Signals для управления данными, поступающими из Firestore:
Шаблон компонента:
<div>
<h2>Items From Signal</h2>
<ul>
@for (item of dataSignal(); track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>Loading...</li>
}
</ul>
<h2>Items From Observable</h2>
<ul>
@for (item of (data$ | async); track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>Loading...</li>
}
</ul>
</div>
Код компонента:
import { Component, inject, Signal } from '@angular/core';
import { Observable, shareReplay } from 'rxjs';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { collection, collectionData, Firestore } from '@angular/fire/firestore';
interface Item {
id: number;
name: string;
}
@Component({
selector: 'app-fourth-example',
templateUrl: './fourth-example.component.html'
})
export class FourthExampleComponent {
dataSignal: Signal<Item[] | undefined>;
data$: Observable<Item[]>;
db: Firestore = inject(Firestore);
constructor() {
const itemCollection = collection(this.db, 'items');
const collectionData$ = collectionData(itemCollection).pipe(
shareReplay(1),
) as Observable<Item[]>;
// Approach 1
this.data$ = collectionData$.pipe(
takeUntilDestroyed(),
);
// Approach 2
this.dataSignal = toSignal(collectionData$);
}
}
Этот пример демонстрирует, как можно использовать Signals и Observables для работы с данными, поступающими из Firestore, что позволяет выбирать подход в зависимости от потребностей приложения.
Подход 1: Подписка на Observable и сохранение его значения в локальной привносит необходимость отписываться от подписки в ngOnDestroy и кешировать значение, что бы избежать проблем с производительностью.
Подход 2: Вместо подписки на Observable, мы можем установить значение Signal напрямую. Сигналы автоматически освобождают ресурсы, когда компонент уничтожается, что устраняет риск утечек памяти,который может возникать при работе с Observable, если вы забыли отписаться от подписки.
Таким образом Signals могут быть полезны для управления локальным состоянием компонента, когда данные приходят из внешнего источника, например Firestore.
Заключение
Новый RxJS Interop в Angular 18 предоставляет мощный инструмент для интеграции Signals и Observables, упрощая управление реактивным состоянием. Использование Signals позволяет сократить сложность асинхронного кода и улучшить производительность приложения, делая его более предсказуемым и легко поддерживаемым.
Angular 18 открывает новые возможности для управления состоянием, и теперь разработчики могут использовать лучший подход для каждого конкретного сценария, комбинируя мощь Observables и простоту Signals. Это делает процесс разработки более удобным и сосредотачивает усилия на создании качественного пользовательского опыта.
Более того Сигналы легче интегрируются с различными частями приложения и позволяют избежать проблем, связанных с подписками (unsubscribe) на Observable и также могут быть полезны для управления локальным состоянием компонента, когда данные приходят из внешнего источника, например Firestore.
Исходный код можно найти на GitHub, а живую демонстрацию кода в действии можно найти здесь.