С выпуском Angular 18 команда разработчиков расширила функциональность RxJS Interop, что значительно упрощает интеграцию между Signals и RxJS Observables, оптимизируя производительность и улучшая читаемость кода. В этой статье мы рассмотрим, что такое RxJS Interop и как он влияет на разработку на Angular.

Эволюция RxJS Interop в Angular

RxJS Interop впервые был представлен в Angular 16 для преодоления разрыва между Signals и RxJS Observables. Первая версия позволяла разработчикам конвертировать Signals в Observables и наоборот. В Angular 17 функциональность была улучшена, чтобы сделать преобразования более эффективными и обеспечить лучшую интеграцию операторов RxJS с Signals.

Теперь, с Angular 18, функция RxJS Interop вышла на новый уровень, предлагая улучшенную поддержку операторов, более качественные преобразования и более гибкие настройки, что делает управление реактивным состоянием ещё более удобным.

Что такое RxJS Interop?

RxJS Interop позволяет разработчикам Angular легко комбинировать и конвертировать Signals и Observables. Традиционно Angular активно использовал RxJS для обработки асинхронных операций, таких как HTTP-запросы или пользовательские события. С появлением Signals Angular предлагает ещё один способ управления реактивным состоянием.

RxJS Interop позволяет:

  • Конвертировать Signals в Observables и наоборот.

  • Использовать операторы RxJS, такие как map, filter и merge, с Signals.

  • Упрощать работу с реактивными данными.

  • Использовать метод outputToObservable для прямых преобразований.

  • Применять гибкие настройки конверсии.

  • Управлять освобождением ресурсов с помощью manualCleanup.

  • Задавать начальное значение для Signals из Observables с помощью initialValue.

Эта функция предоставляет разработчикам больше возможностей для управления реактивными паттернами, сохраняя при этом поддерживаемость приложений.

Краткий обзор 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

Конвертация Signals в Observables

С RxJS Interop вы можете конвертировать Signal в Observable и использовать операторы RxJS, такие как map или debounceTime, для преобразования данных. Также можно применять опции, такие как requireSync, для синхронного поведения:

import { Component } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { createSignal } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <button (click)="incrementSignal()">Increment Signal</button>
      <p>Signal Value: {{ mySignal }}</p>
    </div>
  `
})
export class AppComponent {
  private _mySignal = createSignal(0);
  myObservable: Observable<number>;
  
  constructor() {
    this.myObservable = toObservable(this._mySignal, { requireSync: true });
  }
  
  get mySignal() {
    return this._mySignal();
  }
  
  incrementSignal() {
    this._mySignal.set(this._mySignal() + 1);
  }
}

Конвертация Observables в Signals с manualCleanup и initialValue

Вы можете конвертировать Observable в Signal, упростив управление состоянием, и использовать manualCleanup и initialValue для большего контроля:

import { Component, OnDestroy } from '@angular/core';
import { fromObservable } from '@angular/core/rxjs-interop';
import { Observable, of } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <p>Signal from Observable: {{ mySignal }}</p>
    </div>
  `
})
export class AppComponent implements OnDestroy {
  myObservable: Observable<number> = of(42);
  private _mySignal;
  private cleanupFn: () => void;
  
  constructor() {
    const [signal, cleanup] = fromObservable(this.myObservable, { initialValue: 0, manualCleanup: true });
    this._mySignal = signal;
    this.cleanupFn = cleanup;
  }
  
  get mySignal() {
    return this._mySignal();
  }
  
  ngOnDestroy() {
    this.cleanupFn();
  }
}

Использование outputToObservable

outputToObservable позволяет конвертировать выходные данные компонента Angular в Observables:

import { Component, EventEmitter, Output } from '@angular/core';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <button (click)="onButtonClick()">Click Me</button>
      <p>Check console for button click events</p>
    </div>
  `
})
export class AppComponent {
  @Output() buttonClicked = new EventEmitter<void>();
  buttonClicked$: Observable<void>;
  
  constructor() {
    this.buttonClicked$ = outputToObservable(this, 'buttonClicked');
  }
  
  onButtonClick() {
    this.buttonClicked.emit();
  }
}

Использование операторов RxJS с Signals

Операторы RxJS могут применяться непосредственно к Signals, упрощая код, улучшая производительность и упрощая отладку:

import { Component } from '@angular/core';
import { createSignal } from '@angular/core';
import { map } from 'rxjs/operators';

@UntilDestroy()
@Component({
  selector: 'app-root',
  template: `
    <div>
      <button (click)="incrementSignal()">Increment Signal</button>
      <p>Mapped Signal Value: {{ mappedSignal | async }}</p>
    </div>
  `
})
export class AppComponent {
  private _mySignal = createSignal(0);
  
  get mappedSignal() {
    return this._mySignal().pipe(map(value => value * 2));
  }
  
  get mySignal() {
    return this._mySignal();
  }
  
  incrementSignal() {
    this._mySignal.set(this._mySignal() + 1);
  }
}

Переход к синхронной реактивности с Signals

Хотя RxJS Interop в Angular 18 обеспечивает мощную гибкость, он также открывает дверь к новому подходу — отказу от асинхронной реактивности и переходу к синхронной реактивности с Signals. Возможность эффективно использовать Signals снижает сложность за счёт управления состоянием синхронно, устраняя необходимость в шаблонном коде, связанном с асинхронностью.

Почему стоит рассмотреть синхронную реактивность?

Традиционный подход с использованием Observables и RxJS отлично подходит для обработки сложных асинхронных задач, таких как HTTP-запросы или взаимодействия с UI. Однако этот подход часто включает сложное управление подписками и потенциальные утечки памяти. Переход на Signals позволяет упростить управление реактивным состоянием.

С Signals поток данных проще для понимания, а обновления происходят синхронно, что позволяет разработчикам полагаться на немедленное отражение изменений без необходимости управления асинхронными временными интервалами. Этот сдвиг особенно полезен в сценариях с предсказуемыми и управляемыми потоками данных, что снижает сложность управления Observables.

Пример: Замена RxJS на Signals в сценарии с Firebase

Рассмотрим пример, аналогичный тому, как можно управлять взаимодействием с Firebase. Традиционно разработчики используют RxJS для обработки обновлений в реальном времени и управления подписками, но с Signals мы можем заменить этот паттерн на более простой, синхронный подход.

Пример использования синхронных Signals:

import { Component } from '@angular/core';
import { createSignal } from '@angular/core';
import { Firestore, collectionData, collection } from '@angular/fire/firestore';
import { Observable, Subject } from 'rxjs';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>Items</h2>
      <ul>
        <li *ngFor="let item of data">{{ item.name }}</li>
      </ul>
    </div>
  `
})
export class AppComponent {
  private destroy$ = new Subject();
  private _dataSignal = createSignal<any[]>([]);
  data$!: Observable<any[]>;
  
  constructor(private firestore: Firestore) {
    const col = collection(firestore, 'items');
    this.data$ = collectionData(col);
    // Вместо подписки на Observable, мы можем установить значение Signal напрямую.
    this.data$.pipe(untilDestroyed(this)).subscribe(data => {
      this._dataSignal.set(data);
    });
  }
  
  get data() {
    return this._dataSignal();
  }
}

В этом примере вместо обработки асинхронных подписок и ручного управления состоянием Signal обновляется синхронно при поступлении новых данных. Это делает компонент проще, понятнее и легче в обслуживании.

Заключение

Новый RxJS Interop в Angular предоставляет мост для перехода между реактивными парадигмами, а также открывает путь к синхронной реактивности с использованием Signals. Используя Signals, вы можете упростить архитектуру приложения, снизить сложность управления асинхронными потоками и создать более предсказуемый и поддерживаемый код. Для многих сценариев управления состоянием синхронная реактивность оказывается более чем достаточной и помогает сосредоточиться на самом важном — создании качественного пользовательского опыта.

Комментарии (2)


  1. xemos
    18.10.2024 05:40

    1) С учётом того что EventEmitter наследуется от Observable outputToObservable выглядит бесполезно

    2) Что такое @UntilDestroy(), откуда он импортирован?

    3) "В этом примере вместо обработки асинхронных подписок и ручного управления состоянием Signal обновляется синхронно при поступлении новых данных. Это делает компонент проще, понятнее и легче в обслуживании. "

    Что стало проще? Что понятнее? можно было просто написать <li *ngFor="let item of data$ | async">{{ item.name }}</li>

    и не плодить лишний код.

    4)

    get data() { return this._dataSignal(); }

    Какой смысл так писать? Что мешает вызывать сигнал напрямую в шаблоне? callstack короче будет и меньше лишнего кода.

    5)

    >import { createSignal } from '@angular/core';

    В angular 18 нет импорта по этому пути, есть "import {createSignal} from '@angular/core/primitives/signals';" но у него нет метода pipe.

    google так же ничего не знает про метод pipe у сигналов.

    Статья мусорная, содержит не рабочий код.


    1. MaNaXname
      18.10.2024 05:40

      Ну что Вы хотите. Джипити наше все!