На мою прошлую публикацию был дан всего один комментарий, но зато какой!

Отличный материал! Что бы быстро вникнуть в суть после легаси )

Броооо! Ты не представляешь до какой степени ты прав. Это действительно взгляд на ситуацию со стороны человека который 2 года провел на легасевом необитаемом острове и теперь обозревает окрестности.

А теперь детишки, мы продолжим наше знакомство с текущей ситуацией и на эту статью меня подвело исследование ситуации которая сложилась со State Management в Angular.

Достаточно долгое время выбор был достаточно простым:

  • ты решал не заплывать в бурное море и делал свой на базе RxJS + Services.

  • ты вздыхал и начинал грузить бойлерплейт бочками для NgRX.

  • ты брал NGXS и тоже грузил бойлерплейт, но не по самые шканцы и оставалось еще место чтоб втиснуть в каюту свое тщедушное тельце.

  • в приступе оптимизма ты ловил свежий ветер и шел к Akita.

Причем первые 2 варианта были в явных лидерах. Но прежде чем скомандовать "По местам стоять, с якоря сниматься" посмотрим на карту и коротко пройдемся по всем вариантам. А потом взглянем на обновления.

Тихие воды Service + BehaviorSubject

interface User {
  name: string;
  lastName: string;
  isActive: boolean;
  existDate: Date;
}

@Injectable()
export class UserService {
  private http = inject(HttpClient);

  private _data = new BehaviorSubject<User[] | null>(null);

  get data$(): Observable<User[]>{
    return this._data
      .asObservable()
      .pipe(filter((user): user is User[] => user !== null));
  }
  get getActiveUsers$(){
    return this.data$.pipe(
      map((users: User[]) => users.filter(user => user.isActive))
    )
  }
  private set data$(data: User[]){
    this._data.next(data);
  }
  private addUser(user: User){
    const users: User[] = this._data.getValue() as User[];
    users?.push(user);
    this.data$ = users;
    this.addUserRequest(user);
  }

  loadUsers() {
    this.http.get<User[]>(`/api/users`)
      .subscribe(users => this.data$ = users);
  }
  addUserRequest(user: User){
    this.http.put<User>(`/api/users`, user)
      .subscribe(response => console.log(response))
  }
}

Достаточно стандартный подход начиная от ранних версий и до текущего момента. Все просто, понятно, прозрачно. Все под контролем и не нужно раздувать список зависимостей.

Из минусов: нужно помнить о необходимости отписок и некоторые промежуточные состояния трудно дебажить.

Еще существует утверждение что при разрастании такой вид стора начинает приобретать лапшеобразность. Утверждение все таки достаточно спорное. Большие сторы при отсутствии хорошего архитектора (а лучше всего чтоб он был еще президентом местного клуба самых нудных душнил) часто становятся трудно читаемыми и лапшеобразными вне зависимости от выбранной технологии. 

Не всякий клад стоит искать или почему не всегда нужен NgRx

NgRx задумывался как ответ на хаос. Redux паттерн, единый стор, неизменяемость, DevTools всё это реально помогало на больших проектах. Цена была высокой. Даже простое действие требовало:

action → reducer → effect (если асинхронный) → selector → компонент

ПЯТЬ ФАЙЛОВ для того чтобы правильно провести данные по одной сущности. Этого достаточно что бы джуниоры падали в обморок. Для всех проектов кроме большого и сложного энтерпрайза это был явный перебор.

Хватало случаев что в какой то команде приходили к решению на использование NgRX (или решение приходило сверху) в тех случаях где хватило бы Service + BehaviorSubject и потом лились кровавые слезы.

Отсюда по большей части и растут ноги репутации “NgRX - это очень сложно и много бойлерплейта”. Несмотря на то что бойлерплейта действительно много, сложность все таки значительно преувеличена. Комарадес это вы еще не ходили в React и не знакомились с такой вещью как Redux-Saga

Через рифы асинхронности. NGXS

Это тоже реализация паттерна Redux, тоже однонаправленный поток данных, единый источник истины. Но большое количество бойлерплейта упрятано под капот, меньше шаблонного кода по сравнению с NgRX. Прямо из коробки асинхронный подход, обработчики могут возвращать Observable или PromiseNGXS автоматически подпишется на них и будет ждать завершения, что упрощает работу с HTTP-запросами. Можно использовать привычный подход с setState, операторы состояния (state operators) для более декларативного обновления или даже интегрировать библиотеку для работы с иммутабельностью, например immer. Есть большой выбор плагинов. Например, плагин ngxs-requests-plugin упрощает управление состоянием HTTP-запросов (загрузка, ошибки)

Основные ключевые концепции те же:

  • Store : Глобальный иммутабельный объект,является единым источником истины для всего приложения.

  • State : Класс который управляет определенным срезом (slice) глобального состояния. Он скрывается под декоратором @State, ему присваивается уникальное имя и значение по умолчанию. 

  • Actions : Это простые методы класса которые описывают что должно произойти или уже произошло. Для обработки действия внутри класса состояния используется декоратор @Action. Метод-обработчик получает контекст StateContext, через который можно читать и изменять состояние.

@State<string[]>({
  name: 'animals',
  defaults: []
})
@Injectable()
export class AnimalsState {
  // Обработчик действия AddAnimal
  @Action(AddAnimal)
  addAnimal(ctx: StateContext<string[]>, action: AddAnimal) {
    // Получаем текущее состояние
    const state = ctx.getState();
    // Устанавливаем новое состояние
    ctx.setState([...state, action.animal]);
  }
}
  • Selectors: Функции которые прячутся за декоратором @Selector, и позволяют извлекать определенные части данных из Store.  Селекторы в NGXS могут быть мемоизированы, что повышает производительность, так как вычисления происходят только при изменении данных. Для упрощения их создания есть вспомогательные утилиты, например createPropertySelectors, которые автоматически генерируют селекторы для всех свойств состояния.

@State<AnimalsStateModel>({ ... })
@Injectable()
export class AnimalsState {}

export class AnimalsSelectors {
  // Создает селекторы для zebras, pandas и monkeys
  static getSlices = createPropertySelectors<AnimalsStateModel>(AnimalsState);

  // Использование одного из них
  @Selector([AnimalsSelectors.getSlices.zebras])
  static getZebras(zebras: string[]) { return zebras; }
}
  • Actions Stream : Это RxJS Observable, через который проходят все отправленные действия. Вы можете подписаться на него, чтобы реагировать на жизненный цикл действий (ofActionSuccessful, ofActionErrored и т.д.), например, показывать уведомления после успешного выполнения операции.

В отличие от NgRx, который строго следует паттерну Redux с раздельными редьюсерами, эффектами и действиями, NGXS предлагает более простой и интуитивный подход, объединяя логику в классах состояния. Это делает NGXS хорошим выбором для среднего и крупного проекта, где важна скорость разработки, декларативный подход, но по каким то причинам не лежит душа к NgRx.

В целом, если вы ищете мощное, но более лаконичное решение для управления состоянием в Angular, NGXS отлично подойдет.

Корабль больших надежд. Akita

В свое время позиционировалась как еще более простая альтернатива NgRx 

Вот на чем строилась архитектура Akita:

  1. Store: Это класс, который содержит и управляет состоянием (state). Akita предлагает два основных типа хранилищ:

    • Store : для простого, неструктурированного состояния (например, настройки интерфейса) .

    • EntityStore : для управления коллекциями сущностей (например, список пользователей или задач). Это хранилище предоставляет встроенные методы для операций CRUD (addupdateremoveset), что очень удобно для работы со списками .

  2. Query: Классы, наследующие от Query или QueryEntity, которые служат для получения данных из хранилища в реактивном стиле через RxJS Observable. Они позволяют создавать селекторы для фильтрации, сортировки и комбинирования данных .

  3. Service: Это необязательный слой, который выступает в роли посредника. Компоненты вызывают методы сервиса, а тот, в свою очередь, выполняет бизнес-логику и обновляет хранилище. Такой подход помогает отделить логику обновления от компонентов.

Основная идея Akita заключалась в том чтоб предоставить разработчикам интуитивно понятный и гибкий API и сократить объем шаблонного кода по сравнению с более строгими реализациями Redux, (заявлялась что код можно сократить на 60%).EntityStore с готовыми методами для работы с сущностями и менее строгая архитектура обещали значительно упростить жизнь.

НО! Официальный репозиторий Akita на GitHub был заархивирован, а в документации указано, что библиотека больше не поддерживается . Разработчики рекомендуют перейти на новый проект Elf, который должен прийти на смену Akita .

А что с проектом Elf?

Она создана тем же автором, что и Akita, и если вам нравится то что было реализовано там, то лучше отдать предпочтения именно ему по ряду причин.

Это реактивное, иммутабельное хранилище состояний, построенное на RxJS. Его ключевая идея это простота и гибкость без лишнего шаблонного кода. Вместо монолитного подхода, Elf предлагает модульную архитектуру, где вы подключаете только необходимые функционал.

Для запроса данных используются RxJS-операторы, а для обновления чистые функции, что делает код предсказуемым и легко тестируемым.

Как и в Akita, в Elf есть первоклассная поддержка сущностей (withEntities), которая предоставляет удобные методы для CRUD-операций.

Библиотека предлагает широкий набор готовых решений (плагинов), которые подключаются по мере необходимости благодаря продуманной архитектуре:

  • Persist State: Позволяет легко сохранять состояние хранилища в localStorage или sessionStorage, чтобы восстанавливать его после перезагрузки страницы. Можно гибко настраивать, какие именно части состояния сохранять, исключать конфиденциальные данные и даже использовать асинхронные хранилища вроде IndexedDB.

  • Sync State: Обеспечивает синхронизацию состояния одного и того же хранилища между разными вкладками или окнами браузера с помощью Broadcast Channel API.

  • Другие плагины: Также доступны инструменты для работы с историей состояний, пагинацией, статусами запросов и интеграцией с Devtools.

По данным 2022 года, Elf был менее популярен, чем "гиганты" вроде NgRx (486k загрузок в неделю) или NGXS (118k), но уже имел свою аудиторию (~7.8k загрузок в неделю и 1k звезд на GitHub). Но и этот корабль пал, последний комит был 2 года назад.

Сокровище на десерт

Еще в июле 2024 года, когда библиотека вышла из стадии "developer preview", ее еженедельные загрузки уже достигли 50,000 . Это показывает невероятно быстрый старт.

А в 2026 году она и уверенно тянет ~793,000 - 857,000. Я сейчас говорю про NgRx Signal Store.

Ключевые концепции и API

Основой Signal Store является функция signalStore, которая действует как "конвейер", принимающий последовательность "фич" (features) для построения хранилища . Каждая фича добавляет определенную функциональность .

Вот основные строительные блоки (фичи), которые можно комбинировать для создания хранилища:

  1. withState: Добавляет срез состояния. Каждое свойство состояния автоматически становится вложенным сигналом (глубоким сигналом), что позволяет подписываться на изменения отдельных частей .

  2. withComputed: Добавляет вычисляемые сигналы, которые зависят от других сигналов в хранилище и автоматически обновляются при их изменении .

  3. withMethods: Добавляет методы для обновления состояния. Эти методы могут использовать утилиту patchState для частичного обновления, что делает код более читаемым по сравнению с ручным клонированием объектов .

  4. withHooks: Позволяет выполнять пользовательскую логику на этапах инициализации (onInit) и уничтожения (onDestroy) хранилища .

type Todo = { id: string; text: string; completed: boolean };
type TodoState = {
  items: Todo[];
  filter: 'all' | 'active' | 'completed';
  loading: boolean;
};

// Начальное состояние
const initialState: TodoState = {
  items: [],
  filter: 'all',
  loading: false
};

// Стор
export const TodoStore = signalStore(
  { providedIn: 'root' },
  
  // 1. Состояние
  withState(initialState),
  
  // 2. Вычисления
  withComputed(({ items, filter }) => ({
    filteredItems: computed(() => {
      const all = items();
      switch (filter()) {
        case 'active': return all.filter(t => !t.completed);
        case 'completed': return all.filter(t => t.completed);
        default: return all;
      }
    }),
    stats: computed(() => ({
      total: items().length,
      active: items().filter(t => !t.completed).length,
      completed: items().filter(t => t.completed).length
    }))
  })),
  
  // 3. Методы
  withMethods((store, http = inject(HttpClient)) => ({
    // Асинхронная загрузка
    load: rxMethod<void>(
      pipe(
        tap(() => patchState(store, { loading: true })),
        switchMap(() =>
          http.get<Todo[]>('/api/todos').pipe(
            tapResponse({
              next: (items) => patchState(store, { items, loading: false }),
              error: () => patchState(store, { loading: false })
            })
          )
        )
      )
    ),  
  // 4. Жизненный цикл
  withHooks({
    onInit({ load }) {
      load(); // Автозагрузка при создании
    }
  })
);

NgRx Signal Store  это современное, легковесное и официальное решение для управления состоянием в Angular, построенное на основе нативных Angular Signals . В отличие от классического NgRx Store с его действиями, редьюсерами и эффектами, Signal Store требует минимального шаблонного кода и предлагает интуитивно понятный API .

Помимо базовых фич, Signal Store предоставляет мощные инструменты для сложных сценариев:

  • withRxMethod: Для управления побочными эффектами и асинхронными операциями, такими как API-запросы, используется функция rxMethod . Она принимает цепочку RxJS-операторов и возвращает реактивный метод, который можно вызвать со значением, сигналом или Observable .

export const ProductsStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withMethods((store, productsService = inject(ProductsService)) => ({
    // Метод без параметров
    loadProducts: rxMethod<void>(
      pipe(
        // 1. Включаем индикатор загрузки
        tap(() => patchState(store, { isLoading: true })),
        // 2. switchMap отменит предыдущий запрос
        switchMap(() =>
          productsService.getProducts().pipe(
            tapResponse({
              // 3. Обработка успешного ответа
              next: (items) => patchState(store, { items, isLoading: false }),
              // 4. Обработка ошибки
              error: () => patchState(store, { isLoading: false })
            })
          )
        )
      )
    )
  }))
);
  • withEntities: Специализированный плагин для работы с коллекциями сущностей, который предоставляет готовые утилиты для CRUD-операций (addEntityupdateEntityremoveEntity и др.), что значительно упрощает управление списками .

// Вот и весь стор для хранения списка пользователей
export const UserStore = signalStore(
  { providedIn: 'root' },
  withEntities<User>() // <-- Добавляет все необходимое для управления сущностями
);

// а теперь используем его в компоненте
@Component({...})
export class UsersComponent {
  store = inject(UserStore);

  // Пример: загружаем список пользователей
  loadUsers(users: User[]) {
    // Используем хелпер setAllEntities для обновления всего списка
    patchState(this.store, setAllEntities(users));
  }

  // Пример: добавляем одного пользователя
  addUser(newUser: User) {
    patchState(this.store, addEntity(newUser)); // Используем хелпер addEntity
  }

  // Пример: обновляем пользователя
  updateUser(updatedUser: User) {
    // updateEntity обновляет сущность по ее ID
    patchState(this.store, updateEntity({ id: updatedUser.id, changes: updatedUser }));
  }

  // Пример: удаляем пользователя
  deleteUser(id: string) {
    patchState(this.store, removeEntity(id)); // removeEntity удаляет сущность по ID
  }

  // Получение всех сущностей (сигнал)
  get allUsers() {
    return this.store.entities(); // сигнал, содержащий массив всех пользователей
  }
}
  • signalStoreFeature: Позволяет создавать собственные переиспользуемые фичи, инкапсулируя в них общую логику, такую как состояние загрузки, интеграция с localStorage или отображение уведомлений. Это ключевой механизм для построения чистой и поддерживаемой архитектуры . Сообщество активно создает и делится такими фичами .

  • Инкапсуляция состояния: Начиная с версии 18, состояние хранилища по умолчанию защищено от внешних модификаций, что гарантирует предсказуемый поток данных. Это повышает надежность кода .

  • Приватные члены: Любой член хранилища (состояние, вычисляемый сигнал, метод), имя которого начинается с нижнего подчеркивания (_), считается приватным и недоступен извне. Это помогает четко разделять публичный API и внутреннюю реализацию .

  • watchState: Функция, позволяющая синхронно отслеживать все изменения состояния. Это полезно для реализации таких функций, как "отмена действий" (undo/redo) или ведение логов, где важна каждая промежуточная мутация, а не только финальное состояние 

NgRx Signal Store — это не просто альтернатива, а эволюция управления состоянием в Angular, тесно интегрированная с его современными реактивными примитивами. Судя по документации, активности сообщества и темпам развития, это официально рекомендуемый путь для создания новых приложений на Angular.

Итог: никакой войны, только инструменты

Сигналы не замена RxJS

Одна из главных ошибок последнего времени это пытаться переписать вообще всё на сигналы. Это неправильно. RxJS остаётся лучшим инструментом для работы со временем: debounce, throttle, отмена запросов, WebSocket-соединения.

И сейчас наиболее правильная картина организации выглядит так:

  • RxJS  для асинхронных потоков и управления временем

  • Signals  для хранения текущего состояния

Реальный профит по производительности

В одном из реальных проектов (22k строк TypeScript, маркетинговая аналитика) переход на сигналы дал:

  • Сокращение кода управления состоянием на 63% (8400 → 3100 строк)

  • Уменьшение бандла с 38 KB до 18 KB

  • Перерендеры при вводе: с 47 до 3 за одно нажатие клавиши

Миграция: не нужно переписывать всё сразу

Если у вас уже есть проект на NgRx не нужно бросать все силы на переписывание. Наилучшая стратегия:

  1. Новые фичи пишем на SignalStore или простых сигналах

  2. Старые фичи переписываем постепенно, когда они всё равно меняются

  3. Глобальное состояние (auth, permissions) оставляем на классическом NgRx или переводим на Service + Observable

Во время поиска информации мне попалась история одной миграции. В одном из реальных проектов во время миграции существовало три вида стейтов: NgRx + сигналы + один legacy-компонент на Akita. И это нормально.

В 2026 году Angular даёт разработчикам набор инструментов, а не догму. Сигналы не убили NgRx но необходимости его использовать стало еще меньше. RxJS не умер он занял своё законное место на границе с асинхронностью.

Главный принцип: состояние это не одна библиотека на всё приложение. Это выбор правильного инструмента под конкретную задачу. Для локального UI сигналы. Для серверного кэша TanStack Query или toSignal. Для сложных асинхронных сценариев RxJS + Signals. Для глобального состояния с аудитом NgRx Classic или SignalStore , или Elf если нравиться подход RxJS.

И самое важное: не нужно переписывать всё с нуля. Мигрируйте постепенно, фича за фичей. Берегите себя и свою команду.