Angular

NgRx SignalStore

Сервис состояния фичи разросся: десяток приватных сигналов, computed для производных, методы загрузки, ручная нормализация списка по id. Логика рабочая, но шаблонная - один и тот же каркас повторяется в каждой фиче. NgRx SignalStore собирает этот каркас в декларативную композицию: withState задаёт состояние, withComputed выводит производное, withMethods добавляет операции, withEntities берёт на себя коллекцию по id. Получается тот же сервис на сигналах, но без ручной обвязки и с автоматически выведенными типами.

  • NgRx SignalStore - официальная часть экосистемы NgRx, рекомендованный командой способ управлять состоянием на сигналах в современном Angular
  • Фича-сторы в крупных приложениях: список с фильтрами, пагинацией и загрузкой укладывается в один SignalStore вместо ручного сервиса
  • Миграция с классического Redux-стора: команды переносят локальные фича-сторы на SignalStore ради меньшего объёма кода
  • Коллекции сущностей (заказы, пользователи, товары): withEntities даёт нормализованное хранилище по id из коробки
  • Локальные сторы на уровне компонента: SignalStore можно провайдить в компоненте, и он живёт ровно столько, сколько компонент

Предварительные знания

  • Сервис фичи с сигналами и разделение smart/dumb (предыдущий урок)
  • Сигналы: signal, computed, чтение значения вызовом
  • Dependency injection: inject и провайдинг сервиса
  • Архитектура состояния

От Redux-стора NgRx к SignalStore

Классический NgRx появился в 2017 году как порт паттерна Redux на Angular: единый стор, экшены, редьюсеры, селекторы, эффекты. Мощно для крупных приложений, но многословно - простая операция требовала экшена, редьюсера и селектора. С приходом сигналов в Angular 16 команда NgRx переосмыслила подход. SignalStore (стабильный в 2023-2024) строится не на потоке экшенов, а на композиции функций-фич над сигналами. Состояние стало читаемым напрямую как сигналы, а boilerplate экшенов и редьюсеров исчез для большинства случаев.

Анатомия SignalStore: withState и withComputed

SignalStore создаётся функцией signalStore, в которую передаются функции-фичи. Базовая фича - withState: она принимает начальное состояние и превращает каждое его поле в сигнал, выводя типы автоматически. withComputed добавляет производные сигналы, которые читают другие сигналы стора. Результат signalStore - обычный инжектируемый класс, который провайдят как сервис.

После withState поля tasks и filter доступны на сторе как сигналы: store.tasks(), store.filter(). withComputed добавил visible и remaining - они пересчитываются автоматически. Типы выведены из interface TasksState, ничего не приходится приводить вручную. Компонент инжектит стор как любой сервис и читает сигналы прямо в шаблоне.

Никаких селекторов из классического NgRx тут нет. Производное состояние - это computed, читаемое как обычный сигнал. Стор для компонента выглядит как объект с сигналами, а внутренняя композиция функций-фич остаётся деталью определения стора.

Что делает функция-фича withState в SignalStore?

withMethods и patchState: изменение состояния

Состояние SignalStore меняется только через функцию patchState - она принимает стор и частичное обновление и заменяет указанные поля иммутабельно. Операции собираются в withMethods: туда передаётся стор, и возвращается объект методов. Внутри методов можно инжектить другие сервисы (например, HttpClient) через inject, потому что withMethods выполняется в контексте внедрения.

patchState заменяет только переданные поля, остальное состояние остаётся прежним. Метод toggle строит новый массив через map - состояние иммутабельно, мутации на месте нет. Метод load инжектит TaskApi прямо в сигнатуре withMethods как параметр по умолчанию: это идиома SignalStore, чтобы получить зависимость в контексте внедрения без отдельного поля.

Состояние нельзя менять, присваивая что-то сигналам напрямую - они readonly снаружи. Единственный путь - patchState. Это сохраняет иммутабельность и держит все изменения состояния в одном предсказуемом канале, что упрощает отладку и интеграцию с DevTools.

Тип частичного обновления в patchState проверяется по форме состояния. Передать поле, которого нет в TasksState, или неверный тип значения не выйдет - компилятор остановит. Поэтому приведения as и any здесь не нужны и противопоказаны: типобезопасность встроена.

Как корректно изменить состояние в SignalStore и почему именно так?

withEntities: коллекции сущностей по id

Коллекция объектов с id - частый случай: список заказов, пользователей, товаров. Хранить её как простой массив неудобно: поиск и обновление по id требуют перебора. Фича withEntities из @ngrx/signals/entities даёт нормализованное хранилище: внутри сущности лежат в словаре по id, а наружу отдаются сигналы entities (массив) и entityMap. Обновляют коллекцию готовые операции вроде setAllEntities, addEntity, updateEntity, removeEntity.

withEntities выводит сигналы entities, entityMap и ids. Метод load складывает весь список через setAllEntities, а toggle обновляет одну запись через updateEntity по id - без ручного перебора массива. Под капотом коллекция хранится как словарь, поэтому доступ и обновление по id дёшевы, а наружу всё равно доступен массив entities для рендера.

ОперацияЧто делает
setAllEntities(items)Заменяет всю коллекцию
addEntity(item)Добавляет одну сущность
updateEntity({ id, changes })Обновляет поля сущности по id
removeEntity(id)Удаляет сущность по id

Чтобы хранить несколько коллекций в одном сторе или использовать ключ, отличный от id, withEntities принимает именованную конфигурацию (collection и selectId). Это позволяет, например, держать в одном сторе и заказы, и их позиции, не путая сигналы.

Какую задачу решает фича withEntities по сравнению с хранением коллекции как простого массива в withState?

Store против сервиса и против Redux-стора

SignalStore не отменяет ни обычный сервис с сигналами, ни классический Redux-стор NgRx - у каждого своя ниша. Сервис с сигналами хорош для простого состояния одной фичи без шаблонной обвязки. SignalStore выигрывает, когда нужна стандартизированная композиция, коллекции сущностей или единый формат состояния по всей команде. Классический Redux-стор остаётся оправданным для крупных приложений, где ценят строгий поток экшенов, машину состояний и развитую интеграцию эффектов.

  • Сервис с сигналами — Минимум кода для простого состояния одной фичи. Полная свобода формы. Подходит, пока нет коллекций сущностей и потребности в едином каркасе.
  • SignalStore — Декларативная композиция withState/withComputed/withMethods/withEntities. Авто-типы, нормализованные коллекции, единый формат. Без экшенов и редьюсеров.
  • Классический Redux-стор — Экшены, редьюсеры, селекторы, эффекты. Строгий поток и аудируемость. Многословен, оправдан в крупных приложениях со сложными побочными эффектами.

Практический критерий выбора: начинать стоит с сервиса на сигналах. Как только в нескольких фичах повторяется один и тот же каркас состояния, появляются коллекции по id или команде нужен единый стандарт - это сигнал перейти на SignalStore. К полному Redux-стору обращаются, когда поток изменений настолько сложен, что строгая дисциплина экшенов и редьюсеров окупает свою многословность.

SignalStore можно провайдить не только в root, но и на уровне компонента через providers. Тогда стор живёт ровно столько, сколько компонент, и служит локальным стором фичи - удобная середина между чисто локальными сигналами и глобальным состоянием.

Когда переход с обычного сервиса на сигналах к SignalStore оправдан сильнее всего?

Связь с другими темами

Урок продолжает линию управления состоянием. Связи:

  • Архитектура состояния — SignalStore - это формализованный сервис фичи с сигналами из предыдущего урока
  • Строгая типизация — Стор выводит типы из начального состояния, и строгий режим делает доступ к ним безопасным
  • Тестирование — withMethods даёт обычные функции над сигналами, удобные для unit-тестов в Vitest

Итог

  • SignalStore из @ngrx/signals строит состояние на сигналах через композицию функций-фич
  • withState задаёт исходное состояние и автоматически выводит сигналы и их типы
  • withComputed добавляет производные сигналы, withMethods - операции над состоянием через patchState
  • withEntities даёт нормализованную коллекцию по id с готовыми сигналами entities и operations
  • В отличие от классического Redux-стора NgRx, нет экшенов, редьюсеров и селекторов - состояние читается напрямую как сигналы
  • Store выигрывает у обычного сервиса, когда нужна стандартизированная композиция, коллекции сущностей или общий формат состояния в команде; для простого случая достаточно сервиса с сигналами

Связанные уроки

  • ng-38-state-architecture — SignalStore - это эволюция сервиса с сигналами из урока об архитектуре состояния
  • ng-41-typescript-angular — SignalStore выводит типы состояния автоматически, и строгая типизация раскрывает его сильнее
  • ng-40-testing — Методы стора - обычные функции над сигналами, их легко проверять в Vitest
NgRx SignalStore

0

1

Войти