Angular Signals + RxJS: объединяем два реактивных мира в одном стейт-менеджере
- воскресенье, 10 августа 2025 г. в 00:00:09
 
Signals против RxJS? Нет, вместе — они сила. Теория, практика и готовый state-manager для Angular 17 и выше
Angular долгое время ассоциировался с RxJS. Даже слишком: многие разработчики ощущали, что без Observable ничего не работает. Но вот в Angular 17 появляются Signals — синхронная реактивность прямо из коробки. В 17+ — они становятся мейнстримом. Возникает вопрос: а что делать с RxJS? Выбрасывать?
Signals и RxJS — не конкуренты, а два мощных инструмента для решения разных задач. И если их правильно сочетать, можно построить удобную, масштабируемую и эффективную архитектуру
В этой статье мы:
Разберёмся в различиях между Signals и RxJS
Покажем, когда использовать что
Сделаем свой собственный state-manager с красивым API
И покажем, как всё это выглядит в реальном Angular-приложении
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 — это асинхронные потоки данных. Вы можете описывать сложные цепочки событий, работать с HTTP, WebSocket, таймерами, реакцией на пользовательские действия
const clicks$ = fromEvent(button, 'click');
clicks$.pipe(throttleTime(500)).subscribe(() => console.log('Click!'));Характеристика  | Signal  | Observable (RxJS)  | 
|---|---|---|
Push или Pull  | Pull (pull-based)  | Push  | 
Синхронность  | ✅ Синхронный  | ❌ Асинхронный  | 
Ленивая инициализация  | ❌ Нет  | ✅ Да  | 
Отписка  | ❌ Не требуется  | ✅ Требуется  | 
Трассировка зависимостей  | ✅ Да  | ❌ Нет  | 
Использование в шаблоне  | ✅ Прямо как   | ⚠️ Через   | 
Best fit  | UI состояние  | Потоки событий, async логика  | 
Представь, что у тебя есть UI-состояние (счётчик, фильтр, текущий пользователь) — здесь Signals чувствуют себя как дома. Но вот приходит push-уведомление, пользователь кликает слишком быстро, идёт запрос на сервер — это уже RxJS
Комбо даёт следующее:
Signals для UI и локального состояния
RxJS для событий, побочных эффектов, async и серверного общения
Мост между ними — наш state-manager
Создать createStore, который:
управляет состоянием
позволяет "выбирать" конкретные поля через сигналы
поддерживает .effect() и .select()
использует RxJS внутри (для расширения)
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!'));
});
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-приложения