RxJS Interop в Angular 18: основные изменения и преимущества
- суббота, 19 октября 2024 г. в 00:00:04
С выпуском Angular 18 команда команда разработчиков значительно расширила функциональность RxJS Interop, что упрощает интеграцию между Signals и RxJS Observables, оптимизируя производительность и улучшая читаемость кода. В этой статье мы рассмотрим новые возможности RxJS Interop, примеры их применения и объясним, как они помогают сделать ваш код чище и эффективнее.
RxJS Interop впервые был представлен в Angular 16 для преодоления разрыва между Signals и RxJS Observables. Первая версия позволяла разработчикам конвертировать Signals в Observables и наоборот. В Angular 17 функциональность была улучшена, чтобы сделать преобразования более эффективными и обеспечить лучшую интеграцию операторов RxJS с Signals.
С выпуском Angular 18 функциональность RxJS Interop значительно расширилась, предлагая улучшенную поддержку операторов, более гибкие настройки и дополнительные возможности, что делает управление реактивным состоянием ещё более удобным.
RxJS Interop позволяет разработчикам Angular легко комбинировать и конвертировать Signals и Observables. Традиционно Angular активно использовал RxJS для обработки асинхронных операций, таких как HTTP-запросы или пользовательские события. С появлением Signals Angular предлагает ещё один способ управления реактивным состоянием, который помогает упростить реактивные паттерны.
RxJS Interop позволяет:
Конвертировать Signals в Observables и наоборот.
Использовать операторы RxJS, такие как map
, filter
и merge
, с Signals.
Упрощать работу с реактивными данными.
Использовать более гибкие настройки конверсии и управления состоянием.
Эти функции предоставляют разработчикам больше возможностей для управления реактивными паттернами, сохраняя при этом поддержку приложений на высоком уровне.
RxJS лежит в основе реактивной системы Angular, предоставляя тип Observable для управления асинхронными данными. Он используется во многих частях Angular, таких как HttpClient
, формы и обработка событий.
Observable: поток данных, который генерирует множество значений с течением времени.
Subject: Observable, который рассылает значения нескольким наблюдателям.
BehaviorSubject: хранит последнее значение и немедленно отправляет его новым подписчикам.
ReplaySubject: повторяет буфер предыдущих значений для новых подписчиков.
Эти паттерны мощные, но могут быть сложными при управлении состоянием или обработке нескольких асинхронных операций. Signals в сочетании с RxJS Interop помогают упростить такие сценарии.
С Angular 18 разработчики получают больше гибкости в выборе между Signals и Observables:
Observables идеальны для непрерывных событийных данных, таких как HTTP-запросы или события форм.
Signals лучше подходят для реактивного состояния с предсказуемыми потоками данных.
RxJS Interop делает интеграцию между этими двумя системами бесшовной, повышая возможность повторного использования кода.
В этом примере мы создаем 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, 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 = 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 приложения.
Этот пример демонстрирует передачу значений от дочернего компонента к родительскому с использованием Signals и Observables:
Родительский компонент:
<div>
<p>Check Child Observable Value In Parent: {{ myChildOutputtedSignal() }}</p>
<app-second-example-child></app-second-example-child>
</div>
import { AfterViewInit, Component, signal, ViewChild } from '@angular/core';
import { SecondExampleChildComponent } from './second-example-child/second-example-child.component';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
@UntilDestroy()
@Component({
selector: 'app-second-example',
templateUrl: './second-example.component.html',
})
export class SecondExampleComponent implements AfterViewInit {
@ViewChild(SecondExampleChildComponent) private childComponent: SecondExampleChildComponent | undefined;
myChildOutputtedSignal = signal(0);
myChildOutputtedObservable$: Observable<number> | undefined;
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 created in the constructor
this.myChildOutputtedObservable$ = outputToObservable(this.childComponent!.myObservableChange).pipe(
untilDestroyed(this),
);
}
}
Дочерний компонент:
<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 свойство дочернего компонента
В этом примере показано, как переход от использования 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 на основе декораторов.
В последнем примере мы используем Signals для управления данными, поступающими из Firestore:
Шаблон компонента:
<div>
<h2>Items From Signal</h2>
<ul>
<li *ngFor="let item of dataSignal()">{{ item.name }}</li>
</ul>
<h2>Items From Observable</h2>
<ul>
<li *ngFor="let item of (data$ | async)">{{ item.name }}</li>
</ul>
</div>
Код компонента:
import { Component, inject, Signal } from '@angular/core';
import { Observable, shareReplay } from 'rxjs';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { toSignal } from '@angular/core/rxjs-interop';
import { collection, collectionData, Firestore } from '@angular/fire/firestore';
interface Item {
id: number;
name: string;
};
@UntilDestroy()
@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');
// Подход 1
this.data$ = collectionData(itemCollection).pipe(
shareReplay(1),
untilDestroyed(this),
);
// Подход 2
this.dataSignal = toSignal(collectionData(itemCollection));
}
}
Этот пример демонстрирует, как можно использовать 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.
Вы можете ознакомиться с работой кода из этой статьи тут.