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-приложения
ссылка на оригинал статьи https://habr.com/ru/articles/935526/
Добавить комментарий