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 |
|
Синхронность |
✅ Синхронный |
❌ Асинхронный |
|
Ленивая инициализация |
❌ Нет |
✅ Да |
|
Отписка |
❌ Не требуется |
✅ Требуется |
|
Трассировка зависимостей |
✅ Да |
❌ Нет |
|
Использование в шаблоне |
✅ Прямо как |
⚠️ Через |
|
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-приложения
Автор: del4k1
