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-приложения