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

Но сначала затронем тему статьи: как взаимодействовать с сервисами на базе HttpClient при подключении их к компонентам на основе сигналов. Речь идет об управлении переходом от ответов HttpClient на основе Observable к свойствам Signal, используемым в компонентах на основе сигналов.

На фоне дискуссий об использовании API fetch вместо HttpClient хочу повторить свой совет: не стоит отказываться от HttpClient. Он предлагает полезные функции «из коробки», которые мы можем использовать. Помните, что у нас есть инструменты, чтобы без проблем работать с мостом «от Observable к Signal».

Непосредственная подписка RxJS

Первый и очевидный вариант: просто использовать подписку. Http-вызовы являются однозначными и наблюдаемыми, так что не нужно сильно беспокоиться об отписке. Вы также можете использовать паттерны обработки ошибок RxJS (с помощью метода pipe или через обратный вызов ошибки). Вот наиболее очевидный и простой способ решения проблемы.

public data = signal<number[]>([])
....
constructor(private testService: TestService) {}
....
public ngOnInit() {
this.testService.getItems().subscribe((items) => {
// переписывание/создание сигнала
this.data = signal(items);
// или непосредственная установка значения
this.data.set(items);
});
}

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

Второй вариант — задействовать промисы. Можете использовать стандартный синтаксис then/catch или async/await, но этот подход связан с трудностями, особенно если вы забыли или не знаете специфики async/await. Можете также применить функцию RxJS firstValueFrom (поскольку ответы API обычно однозначны, это хороший вариант) для преобразования первого выданного значения в наблюдаемое. Здесь есть два подводных камня.

  • Важно отметить, что использование синтаксиса async/await приостановит выполнение функции до тех пор, пока промис не вернет значение или ошибку. Таким образом, остальная часть кода все еще будет ожидать выполнения. Поэтому не стоит загонять себя в ловушку async/await.
  • Вы также должны знать, что функция firstValueFrom немедленно подпишется на наблюдаемый источник (observable source). Это не должно стать проблемой, но если вы запустите ее в «ленивом» режиме, она может работать не так, как вы ожидаете.
public async ngOnInit() {
// немедленно подпишется на наблюдаемый источник
this.data = signal(await firstValueFrom(this.testService.getItems()));
// внимание! этот код не будет выполнен, пока сервис не вернет значение

Чтобы избежать остановки выполнения кода, можно использовать специальную функцию-обертку. Другой способ — применить IIFE, что, вероятно, будет более последовательным подходом.

public async initData() {
this.data = signal(await firstValueFrom(this.testService.getItems()));
}

public ngOnInit() {
this.initData();
....
}
public ngOnInit() {    
(async () => {
this.data = signal(await firstValueFrom(this.testService.getItems()));
console.log('data inited');
})();
....
}

Альтернативный вариант — использовать старый добрый then. Не забывайте о специфике выполнения обратного вызова then, так как микрозадача (во всех приведенных выше случаях промисов) имеет свою специфику и в некоторых случаях может дать непредсказуемый результат.

public ngOnInit() {   
firstValueFrom(this.testService.getItems()).then((items) => {
this.data = signal(items);
});
}

Использование toSignal из пакета rxjs-interop

Пакет rxjs-interop предоставляет третий способ, который называется toSignal. Это функция, которая подписывается на наблюдаемый источник и передает все значения в сигнал. Однако это самый изощренный способ преобразования ответа HttpClient в сигнал. Поэтому вам следует знать о следующих особенностях.

  • Один из ключевых аспектов, на который следует обратить внимание, заключается в том, что, как и firstValueFrom, toSignal подписывается сразу после выполнения.
  • Она сразу же выдаст первое значение (undefined), если вы не зададите значение по умолчанию.
  • Одна из потенциальных проблем, с которой вы можете столкнуться, — это ‘Error: NG0203. toSignal() может быть использована только в контексте инъекции (как конструктор, фабричная функция, инициализатор поля или функция, используемая с runInInjectionContext). Эта ошибка может возникнуть, если вы делаете вызов API в хуках жизненного цикла или где-то в методах класса компонента. Чтобы решить эту проблему, нужно провести инъекцию и позаботиться об инжекторе или использовать runInInjectionContext.
  • Это даст сигнал только для чтения (read-only). Поэтому, если вы собираетесь взаимодействовать с сигналом в будущем (настраивать/обновлять его), вам следует устранить возможное препятствие.
private _injector = inject(Injector);

public ngOnInit() {
this.data = toSignal(this.testService.getItems(), {
// предоставить ссылку на инжектор
injector: this._injector,
// предоставить значение по умолчанию
initialValue: [],
});

// или использовать runInInjectionContext
runInInjectionContext(this._injector, () => {
toSignal(this.testService.getItems(), {
initialValue: [],
});
});
....
}

Заключение

На данный момент использование стандартной подписки на наблюдаемое значение кажется наиболее безболезненным вариантом, который убережет от возможных подводных камней и сложностей. Задействование промисов может быть удобным, но вы и ваши коллеги должны знать об async/await и других особенностях промисов. А вот использование toSignal выглядит чрезмерно громоздким.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Igor Pak: Angular: making bridges between HttpClient and Signals

Предыдущая статьяСтековая и кучная память в Kotlin 
Следующая статьяNelm — полноценная замена Helm