Angular
Сервисы и состояние на сигналах
Корзина интернет-магазина видна в шапке, на странице товара и в самой корзине. Все три места должны показывать одно и то же количество товаров и одну сумму. Если каждый компонент хранит своё состояние, оно неизбежно разъезжается. Решение - вынести состояние в сервис, единый экземпляр которого внедряется во все компоненты. А если состояние внутри сервиса хранить в сигналах, получается лёгкий стор: данные реактивны, компоненты обновляются сами, и не нужна тяжёлая библиотека управления состоянием для большинства задач.
- Корзина: один сервис хранит товары и сумму, шапка и страница корзины читают их из общего источника
- Аутентификация: сервис держит текущего пользователя, многие компоненты реагируют на вход и выход
- Настройки темы и языка: единый сервис хранит выбор и раздаёт его всему приложению
- Кэш справочников: сервис загружает список городов или категорий один раз и переиспользует его
- Лёгкий стор на сигналах вместо полноценного NgRx там, где состояние простое
Предварительные знания
- Понимание Dependency Injection: провайдеры, инжекторы, функция inject
- Понимание сигналов: signal, computed, asReadonly для отдачи только чтения
- Идея общего состояния: данные, нужные нескольким компонентам одновременно
От тяжёлых сторов к лёгкому состоянию на сигналах
Долгие годы управление общим состоянием в крупных Angular-приложениях означало NgRx - реализацию паттерна Redux с действиями, редьюсерами и селекторами на RxJS. Подход надёжен, но многословен и тяжёл для простых случаев. С приходом сигналов в 2023 году появился более лёгкий путь: хранить состояние прямо в сигналах внутри обычного сервиса. Реактивность сигналов означает, что компоненты обновляются сами, без действий и редьюсеров. Сама команда NgRx выпустила SignalStore, построенный на сигналах. К Angular 21 паттерн сервиса с сигнальным состоянием стал стандартным выбором для умеренной сложности, а полноценный Redux-стор остался для действительно крупных приложений.
Сервис как синглтон через providedIn root
Сервис в Angular - это обычный класс, помеченный декоратором Injectable, предназначенный для логики и данных, которые нужно разделять между компонентами. Самый частый способ зарегистрировать его - providedIn: 'root'. Это создаёт синглтон: корневой инжектор хранит один экземпляр сервиса, и все, кто его запрашивает, получают именно его. Так разные компоненты работают с общим состоянием, а не с собственными копиями.
Здесь HeaderComponent и CartPageComponent получают один и тот же экземпляр CartService. Изменение состояния через один компонент мгновенно видно другому, потому что состояние живёт в общем сервисе. Это прямое применение разделения ответственности: компоненты отвечают за отображение, сервис - за хранение и изменение данных.
providedIn root даёт ещё и tree-shaking: если сервис нигде не внедряется, сборщик исключит его из бандла. Поэтому регистрация в root - не только про синглтон, но и про оптимальный размер итогового приложения.
Что даёт регистрация сервиса через providedIn root?
Сигнальное состояние в сервисе: лёгкий стор
Если хранить состояние сервиса в сигналах, синглтон превращается в реактивный стор. Компоненты читают сигналы сервиса прямо в шаблоне, и при изменении состояния обновляются сами, без ручного оповещения. Производные значения выражаются через computed внутри того же сервиса. Это и есть современный лёгкий паттерн стора: всё состояние в одном месте, реактивное по умолчанию, без действий и редьюсеров.
Состояние - это сигнал items. Производные count и total объявлены как computed и пересчитываются сами при изменении items. Метод add - единственный путь изменить состояние, и он использует update для иммутабельного обновления массива. Компоненту достаточно прочитать cart.count() и cart.total() в шаблоне, и эти значения всегда актуальны.
Для умеренной сложности такой сервис заменяет полноценный Redux-стор. Действия, редьюсеры и селекторы NgRx оправданы для крупных приложений со сложными потоками данных, а для большинства фич хватает сервиса с сигналами и парой методов.
Почему сервис с состоянием в сигналах называют лёгким стором?
Отдача состояния только для чтения
Чтобы стор оставался предсказуемым, состояние должно меняться только через методы сервиса, а не напрямую из компонентов. Для этого перезаписываемые сигналы держат приватными, а наружу отдают версию только для чтения через asReadonly. Производные computed и так доступны лишь для чтения. В результате компонент может читать любое состояние, но изменить его способен только сам сервис через свой публичный API.
- Перезаписываемый сигнал наружу — Любой компонент может вызвать set и изменить состояние в обход методов. Источник истины перестаёт быть предсказуемым
- Readonly наружу плюс методы — Компоненты только читают, а менять состояние можно лишь через login и logout. Поведение стора предсказуемо и тестируемо
Это тот же принцип инкапсуляции, что и приватные поля с публичными методами. Сервис становится единственным владельцем состояния и контролирует все его изменения через явный набор операций. Когда состояние меняется только в одном месте, отлаживать и тестировать приложение заметно проще: поведение каждой операции предсказуемо.
Отдавать перезаписываемый сигнал напрямую наружу - частая ошибка, которая ломает инкапсуляцию стора. Сигнал состояния должен быть приватным, а публичными - только его readonly-версия, производные computed и методы изменения.
Как сервис обеспечивает, чтобы общее состояние менялось только через его методы?
Связь с другими темами
Этот урок соединяет DI и сигналы в практический паттерн общего состояния:
- Dependency Injection — Сервис регистрируется как провайдер и внедряется как единый экземпляр через inject
- Сигналы — Состояние сервиса хранят в сигналах, наружу отдают через asReadonly
- computed — Производные значения стора (итог, количество) выражают через computed
Итог
- Сервис - это класс для логики и состояния, разделяемых между компонентами, регистрируемый через провайдер
- Injectable с providedIn root делает сервис синглтоном: один экземпляр на всё приложение
- Состояние сервиса хранят в сигналах, что делает его реактивным без действий и редьюсеров - это лёгкий стор
- Перезаписываемые сигналы держат приватными, а наружу отдают только чтение через asReadonly, чтобы менять состояние могли лишь методы сервиса
- Производные значения выражают через computed, образуя предсказуемый и тестируемый источник истины для общего состояния
Связанные уроки
- ng-17-di-intro — Сервисы создаются и внедряются через DI, разобранный в предыдущем уроке
- ng-11-signals-intro — Состояние сервиса хранят в сигналах, поэтому нужно понимать signal и asReadonly
- ng-12-computed — Производные значения стора выражают через computed поверх сигнального состояния