Search
Write a publication
Pull to refresh

Angular Signals + RxJS: объединяем два реактивных мира в одном стейт-менеджере

Level of difficultyMedium
Reading time3 min
Views509

Signals против RxJS? Нет, вместе — они сила. Теория, практика и готовый state-manager для Angular 17 и выше

Введение

Angular долгое время ассоциировался с RxJS. Даже слишком: многие разработчики ощущали, что без Observable ничего не работает. Но вот в Angular 17 появляются Signals — синхронная реактивность прямо из коробки. В 17+ — они становятся мейнстримом. Возникает вопрос: а что делать с RxJS? Выбрасывать?

Signals и RxJS — не конкуренты, а два мощных инструмента для решения разных задач. И если их правильно сочетать, можно построить удобную, масштабируемую и эффективную архитектуру

В этой статье мы:

  • Разберёмся в различиях между Signals и RxJS

  • Покажем, когда использовать что

  • Сделаем свой собственный state-manager с красивым API

  • И покажем, как всё это выглядит в реальном Angular-приложении

Signals и RxJS — не вместо, а вместе

Что такое Signal?

Signal — это реактивная, синхронная переменная. Она знает, кто её читает, и автоматически обновляет потребителей при изменении значения. Плюс: интеграция с Change Detection Angular

import {ChangeDetectionStrategy, Component, computed, effect, signal} from '@angular/core';

@Component({
  selector: 'counter',
  imports: [],
  template: `
    <div>
      <h2>Signal Counter Example</h2>
      
      <p>Count: {{ count() }}</p>
      <p>Doubled: {{ doubled() }}</p>
      
      <button (click)="increment()">Increment</button>
      <button (click)="reset()">Reset</button>
    </div>
  `,
  styles: `button { margin-right: 8px; }`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Counter {
  // Создаем сигнал с начальным значением 0
  count = signal(0);
  
  // Вычисляемое значение на основе сигнала
  doubled = computed(() => this.count() * 2);
  
  constructor() {
    // Эффект для логирования изменений
    effect(() => {
      console.log(`Count changed to: ${this.count()}`);
      console.log(`Doubled value is: ${this.doubled()}`);
    });
  }
  
  increment() {
    // Обновляем значение сигнала
    this.count.update(current => current + 1);
  }
  
  reset() {
    // Устанавливаем новое значение
    this.count.set(0);
  }
}

Что такое RxJS?

RxJS — это асинхронные потоки данных. Вы можете описывать сложные цепочки событий, работать с HTTP, WebSocket, таймерами, реакцией на пользовательские действия

const clicks$ = fromEvent(button, 'click');
clicks$.pipe(throttleTime(500)).subscribe(() => console.log('Click!'));

Таблица различий

Характеристика

Signal

Observable (RxJS)

Push или Pull

Pull (pull-based)

Push

Синхронность

✅ Синхронный

❌ Асинхронный

Ленивая инициализация

❌ Нет

✅ Да

Отписка

❌ Не требуется

✅ Требуется

Трассировка зависимостей

✅ Да

❌ Нет

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

✅ Прямо как count()

⚠️ Через async pipe или subscribe

Best fit

UI состояние

Потоки событий, async логика

Как они сочетаются?

Представь, что у тебя есть UI-состояние (счётчик, фильтр, текущий пользователь) — здесь Signals чувствуют себя как дома. Но вот приходит push-уведомление, пользователь кликает слишком быстро, идёт запрос на сервер — это уже RxJS

Комбо даёт следующее:

  • Signals для UI и локального состояния

  • RxJS для событий, побочных эффектов, async и серверного общения

  • Мост между ними — наш state-manager

Практика — Пишем мини state-manager

Создать createStore, который:

  • управляет состоянием

  • позволяет "выбирать" конкретные поля через сигналы

  • поддерживает .effect() и .select()

  • использует RxJS внутри (для расширения)

API

const userStore = createStore({ name: 'Anon', loggedIn: false });

// Signal-селектор
const name = userStore.select('name'); // signal<string>

// Обновление
userStore.update(state => ({ ...state, name: 'Далер' }));

// RxJS эффект
userStore.effect(state$ => {
  state$.pipe(
    filter(state => state.loggedIn)
  ).subscribe(() => console.log('User logged in!'));
});

Реализация createStore

import { signal, computed, effect } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export function createStore<T extends Record<string, any>>(initial: T) {
  const subject$ = new BehaviorSubject<T>(initial);
  const stateSignal = signal<T>(initial);

  // Синхронизация сигнала с Rx
  subject$.subscribe((value) => {
    stateSignal.set(value);
  });

  return {
    select<K extends keyof T>(key: K) {
      return computed(() => stateSignal()[key]);
    },
    update(mutator: (prev: T) => T) {
      const newValue = mutator(stateSignal());
      subject$.next(newValue);
    },
    effect(fn: (state$: Observable<T>) => void) {
      fn(subject$.asObservable());
    },
    // Вдобавок:
    asSignal() {
      return stateSignal;
    },
    asObservable() {
      return subject$.asObservable();
    },
  };
}

Подводные камни

  • Не стоит использовать Signals для async логики. Используй Observable + async pipe

  • Signals — не замена RxJS, а его дополнение. В UI — сигналы, в бизнес-логике — потоки

  • effect() не имеет cancel/unsubscribe. В отличие от subscribe, он работает вечно

Итог

Signals и RxJS — это не «или-или». Это «и-и».
Signals дают реактивность внутри UI. RxJS управляет асинхронностью и потоками.
Вместе они позволяют писать чистые, быстрые, масштабируемые Angular-приложения

Tags:
Hubs:
+1
Comments4

Articles