Управление состоянием на сигналах

Сигнальный стор — это сервис, который прячет состояние за приватным сигналом и отдаёт наружу только производные значения и методы.

«Состояние без дисциплины превращается в кашу. Сигнальный стор — это дисциплина: одна дверь на запись, много окон на чтение».

Когда состояние общее для многих компонентов — корзина, авторизация, тема — его место в сервисе. Сигналы делают такой сервис элегантным стором: внутри живёт приватный 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Производные значения наружу
методыЕдинственная дверь на запись
Проверьте себя
1. Зачем в сигнальном сторе приватный signal отдают наружу через asReadonly()?
AДля ускорения
BЧтобы компоненты могли читать состояние, но не могли менять его в обход методов
CЧтобы сэкономить память
DasReadonly копирует данные на сервер
2. Почему в методах стора используют update(list => [...list, item]), а не list.push(item)?
Apush медленнее
BНужна новая ссылка на массив, иначе сигнал и зависимые computed не заметят изменение
Cpush запрещён в TypeScript
DЭто одно и то же