Управление состоянием на сигналах
Сигнальный стор — это сервис, который прячет состояние за приватным сигналом и отдаёт наружу только производные значения и методы.
«Состояние без дисциплины превращается в кашу. Сигнальный стор — это дисциплина: одна дверь на запись, много окон на чтение».
Когда состояние общее для многих компонентов — корзина, авторизация, тема — его место в сервисе. Сигналы делают такой сервис элегантным стором: внутри живёт приватный signal, наружу торчат computed только для чтения и публичные методы для изменения. Так никто не сможет испортить состояние в обход правил.
import { Injectable, signal, computed } from '@angular/core';
interface Item { id: number; title: string; price: number; }
@Injectable({ providedIn: 'root' })
export class CartStore {
// приватное изменяемое состояние
private items = signal<Item[]>([]);
// публичное только для чтения
readonly all = this.items.asReadonly();
readonly count = computed(() => this.items().length);
readonly total = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0));
// единственная дверь на запись — методы
add(item: Item) {
this.items.update(list => [...list, item]);
}
remove(id: number) {
this.items.update(list => list.filter(i => i.id !== id));
}
clear() { this.items.set([]); }
}
Любой компонент внедряет CartStore и читает store.count(), store.total() — реактивно и без подписок. Состояние одно на приложение (providedIn: 'root'), а изменять его можно только через методы. Это масштабируемая замена сложным библиотекам управления состоянием для большинства приложений.
Как работает под капотом
Метод asReadonly() возвращает обёртку сигнала без set/update — компоненты могут читать, но не писать. update внутри методов создаёт новый массив (через spread/filter) вместо мутации старого: смена ссылки гарантирует, что зависимые computed и шаблоны заметят изменение.
CartStore (синглтон)
private items: signal <- запись только изнутри
| |
asReadonly() computed(count,total)
| |
v v
компонент A компонент B <- только читают
\ /
методы add/remove <- единственная дверь на запись
Частые ошибки
- Делать сигнал публичным и писать в него отовсюду. Теряется контроль — состояние меняют как попало.
- Мутировать массив на месте (
list.push) — сигнал не заметит, ведь ссылка та же. - Тянуть тяжёлую state-библиотеку там, где хватает сигнального стора.
Best practices
- Прячьте записываемый сигнал в
private, наружу давайтеasReadonly()иcomputed. - Меняйте состояние только через методы — это единая точка правил и валидации.
- Работайте иммутабельно: новый массив/объект вместо мутации.
Итоги. Сигнальный стор = приватный signal + публичные computed + методы-мутаторы в сервисе с providedIn: 'root'. Это простой, типобезопасный и масштабируемый способ управлять общим состоянием. Дальше — маршрутизация и формы.
Закрепляем
Сигнальный стор — это паттерн, а не отдельный инструмент: вы просто складываете уже знакомые кусочки в дисциплинированную форму. Внутри сервиса с providedIn: 'root' живёт приватный signal с состоянием. Наружу торчат только computed и asReadonly() для чтения, плюс публичные методы для изменения. Получается «одна дверь на запись, много окон на чтение»: состояние нельзя испортить в обход правил, а значит, его легко отлаживать и тестировать.
Этот скромный паттерн закрывает потребности подавляющего большинства приложений в управлении состоянием — без тяжёлых сторонних библиотек. Корзина, авторизация, тема оформления, фильтры каталога прекрасно живут в сигнальных сторах. Ключевых правил два, и оба вы уже знаете: прячьте записываемый сигнал в private, отдавая наружу только read-only представления, и меняйте состояние иммутабельно, создавая новые массивы и объекты. Соблюдайте их — и состояние вашего приложения останется управляемым даже когда оно вырастет.
| Часть стора | Роль |
|---|---|
| private signal | Изменяемое состояние (скрыто) |
| asReadonly() | Чтение без записи |
| computed | Производные значения наружу |
| методы | Единственная дверь на запись |