NgRx Signal Store vs Класичний NgRx у 2026: Який Обрати?
Комплексне порівняння NgRx Signal Store та класичного NgRx для управління станом в Angular. Функції NgRx 22 з практичними прикладами коду та рекомендаціями.

NgRx Signal Store представляє фундаментальний зсув в управлінні станом Angular, замінюючи традиційний патерн Redux (екшени-редюсери-селектори) реактивним підходом на основі сигналів. З NgRx 22 (випущено в серпні 2026) Signal Store отримує розширення ресурсів, динамічні deep signals та покращену композиційність. Це порівняння досліджує обидва рішення, щоб допомогти розробникам Angular приймати обґрунтовані архітектурні рішення.
Використовуйте Signal Store для стану на рівні фіч, застосунків середньої складності та інтеграції з нативним Resource API Angular. Використовуйте Класичний NgRx для корпоративних застосунків, що вимагають суворих аудиторських слідів, складних побічних ефектів та розширеного налагодження в DevTools.
Розуміння Архітектурних Відмінностей
Класичний NgRx базується на патерні Redux: екшени описують події, редюсери створюють новий незмінний стан, а селектори витягують дані. Кожна зміна стану проходить через передбачуваний, відстежуваний конвеєр.
Signal Store використовує інший підхід. Замість диспатчингу екшенів через редюсери, стан живе в реактивних сигналах. Оновлення відбуваються через методи, що викликають patchState(), а похідні значення отримуються з computed signals замість мемоїзованих селекторів.
| Аспект | Класичний NgRx | Signal Store |
|---|---|---|
| Ментальна модель | Redux/Flux | Реактивні сервіси |
| Бойлерплейт | Високий (екшени, редюсери, селектори, ефекти) | Низький (стан, методи, computed) |
| Реактивність | RxJS Observables | Angular Signals |
| Resource API | Ручна інтеграція | Нативна з @ngrx/signals/resource |
| DevTools | Повні Redux DevTools | Signal Store DevTools (через плагін) |
| Крива навчання | Крута | Помірна |
| Розмір бандлу | Більший (~15-20KB gzip) | Менший (~4KB gzip) |
Для розробників, вже знайомих з Angular Signals, Signal Store відчувається як природне розширення реактивних примітивів фреймворку.
Класичний NgRx: Патерн Redux в Angular
Класичний NgRx організовує управління станом навколо екшенів, редюсерів, селекторів та ефектів. Таке розділення забезпечує передбачуваність ціною багатослівності.
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Book } from './book.model';
export const BooksActions = createActionGroup({
source: 'Books',
events: {
'Load Books': emptyProps(),
'Load Books Success': props<{ books: Book[] }>(),
'Load Books Failure': props<{ error: string }>(),
'Add Book': props<{ book: Book }>(),
'Remove Book': props<{ id: string }>(),
},
});API createActionGroup групує пов'язані екшени під одним джерелом, покращуючи читабельність у DevTools та зменшуючи бойлерплейт порівняно з окремими викликами createAction.
import { createReducer, on } from '@ngrx/store';
import { BooksActions } from './books.actions';
import { Book } from './book.model';
export interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const booksReducer = createReducer(
initialState,
on(BooksActions.loadBooks, (state) => ({
...state,
loading: true,
error: null,
})),
on(BooksActions.loadBooksSuccess, (state, { books }) => ({
...state,
books,
loading: false,
})),
on(BooksActions.loadBooksFailure, (state, { error }) => ({
...state,
loading: false,
error,
}))
);Редюсери залишаються чистими функціями, що повертають нові об'єкти стану. Функція on() лаконічно обробляє зіставлення екшенів та оновлення стану.
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { BooksState } from './books.reducer';
export const selectBooksState = createFeatureSelector<BooksState>('books');
export const selectAllBooks = createSelector(
selectBooksState,
(state) => state.books
);
export const selectBooksLoading = createSelector(
selectBooksState,
(state) => state.loading
);
export const selectPublishedBooks = createSelector(
selectAllBooks,
(books) => books.filter((book) => book.published)
);Селектори автоматично забезпечують мемоїзацію. Селектор selectPublishedBooks перераховується лише коли змінюється масив книг, роблячи читання ефективним навіть у store, що часто оновлюються.
NgRx Signal Store: Управління Станом на Основі Сигналів
Signal Store консолідує стан, computed значення та методи в єдиний ін'єктований сервіс. Офіційна документація NgRx описує його як "повнофункціональне рішення для управління станом з нативною підтримкою Angular Signals."
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { BooksService } from './books.service';
import { Book } from './book.model';
interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const BooksStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ books }) => ({
publishedBooks: computed(() => books().filter((book) => book.published)),
totalBooks: computed(() => books().length),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, { loading: true, error: null });
try {
const books = await booksService.getAll();
patchState(store, { books, loading: false });
} catch (error) {
patchState(store, { loading: false, error: 'Failed to load books' });
}
},
addBook(book: Book) {
patchState(store, ({ books }) => ({ books: [...books, book] }));
},
removeBook(id: string) {
patchState(store, ({ books }) => ({
books: books.filter((book) => book.id !== id),
}));
},
}))
);Все управління станом фічі вміщується в одному файлі. Властивості стану автоматично стають сигналами, computed signals замінюють селектори, а методи замінюють диспатчинг екшенів.
NgRx 22: Розширення Ресурсів та withProps
NgRx 22 вводить точку входу @ngrx/signals/resource, що забезпечує пряму інтеграцію з Resource API Angular. Функція withProps централізує ін'єкцію сервісів, а розширення ресурсів обробляють типові асинхронні патерни.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withProps } from '@ngrx/signals';
import { extendResource, withPreviousValueOnLoading, withValueOnError } from '@ngrx/signals/resource';
import { httpResource } from '@angular/core';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksResourceStore = signalStore(
{ providedIn: 'root' },
// Ін'єкція сервісів через withProps (приватні за конвенцією з підкресленням)
withProps(() => ({
_booksService: inject(BooksService),
})),
// Налаштування ресурсів з розширеннями
withProps((store) => ({
_booksResource: extendResource(
httpResource<Book[]>({
url: '/api/books',
defaultValue: [],
}),
withPreviousValueOnLoading(), // Зберігає останнє значення під час перезавантаження
withValueOnError([]) // Повертає порожній масив при помилці
),
})),
// Експонування ресурсу тільки для читання споживачам
withProps((store) => ({
booksResource: store._booksResource.asReadonly(),
})),
withComputed((store) => ({
books: computed(() => store.booksResource.value()),
isLoading: computed(() => store.booksResource.isLoading()),
publishedBooks: computed(() =>
store.booksResource.value().filter((book) => book.published)
),
}))
);Розширення ресурсів вирішують дві типові проблеми: мерехтіння під час перезавантаження (виправлене withPreviousValueOnLoading) та обробку стану помилки (виправлену withValueOnError або withPreviousValueOnError). Ці розширення компонуються і можуть надаватися глобально через provideResourceExtensions.
Управління Сутностями з withEntities
Обидва підходи підтримують колекції сутностей, але функція withEntities в Signal Store драматично зменшує кількість коду.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods } from '@ngrx/signals';
import {
addEntity,
removeEntity,
setAllEntities,
updateEntity,
withEntities,
} from '@ngrx/signals/entities';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksEntityStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withComputed(({ entities }) => ({
publishedBooks: computed(() => entities().filter((book) => book.published)),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
const books = await booksService.getAll();
patchState(store, setAllEntities(books));
},
addBook(book: Book) {
patchState(store, addEntity(book));
},
updateBook(id: string, changes: Partial<Book>) {
patchState(store, updateEntity({ id, changes }));
},
removeBook(id: string) {
patchState(store, removeEntity(id));
},
}))
);Функція withEntities автоматично надає сигнали ids, entityMap та entities. Оновлювачі сутностей як addEntity, removeEntity та updateEntity обробляють мутації колекцій.
Готовий до співбесід з Angular?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Власні Фічі Store для Повторного Використання
Функція signalStoreFeature в Signal Store дозволяє витягувати та повторно використовувати наскрізну функціональність у різних store. NgRx 22 додає SignalStoreFeatureType для спрощення витягування типів.
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState, SignalStoreFeatureType } from '@ngrx/signals';
type RequestStatus = 'idle' | 'pending' | 'fulfilled' | 'error';
export function withRequestStatus() {
return signalStoreFeature(
withState<{ requestStatus: RequestStatus }>({ requestStatus: 'idle' }),
withComputed(({ requestStatus }) => ({
isPending: computed(() => requestStatus() === 'pending'),
isFulfilled: computed(() => requestStatus() === 'fulfilled'),
isError: computed(() => requestStatus() === 'error'),
}))
);
}
// NgRx 22: витягування типів без ручного перевизначення
export type RequestStatusFeature = SignalStoreFeatureType<typeof withRequestStatus>;
export function setPending(): { requestStatus: RequestStatus } {
return { requestStatus: 'pending' };
}
export function setFulfilled(): { requestStatus: RequestStatus } {
return { requestStatus: 'fulfilled' };
}
export function setError(): { requestStatus: RequestStatus } {
return { requestStatus: 'error' };
}Цю власну фічу тепер можна скомпонувати в будь-який store, що потребує відстеження стану завантаження.
import { inject } from '@angular/core';
import { patchState, signalStore, withMethods } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { withRequestStatus, setFulfilled, setPending, setError } from './with-request-status';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withRequestStatus(),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, setPending());
try {
const books = await booksService.getAll();
patchState(store, setAllEntities(books), setFulfilled());
} catch {
patchState(store, setError());
}
},
}))
);Скомпонований store тепер надає computed signals isPending(), isFulfilled() та isError() без дублювання логіки.
Синхронне Відстеження Стану з watchState
NgRx 22 додає функцію watchState для синхронного відстеження змін стану, корисну для функціональності undo/redo або історії стану.
import { inject, DestroyRef } from '@angular/core';
import { watchState } from '@ngrx/signals';
import { BooksStore } from './books.store';
export function setupStateHistory() {
const store = inject(BooksStore);
const destroyRef = inject(DestroyRef);
const history: unknown[] = [];
// Синхронне відстеження: захоплює кожну зміну стану, без об'єднання
watchState(store, (state) => {
history.push(structuredClone(state));
});
return { history };
}На відміну від відстеження на основі ефектів, яке об'єднує оновлення, watchState спрацьовує синхронно при кожному виклику patchState, захоплюючи повну історію стану.
Патерни Інтеграції з Компонентами
Signal Store природно інтегрується з синтаксисом шаблонів Angular через читання сигналів.
import { Component, inject } from '@angular/core';
import { BooksStore } from './books.store';
@Component({
selector: 'app-books-list',
standalone: true,
template: `
@if (store.isPending()) {
<div class="loading-spinner">Завантаження книг...</div>
}
@if (store.isError()) {
<div class="error-message">Не вдалося завантажити книги</div>
}
@for (book of store.entities(); track book.id) {
<div class="book-card">
<h3>{{ book.title }}</h3>
<p>{{ book.author }}</p>
<button (click)="store.removeBook(book.id)">Видалити</button>
</div>
}
<p>Всього: {{ store.totalBooks() }} книг</p>
`,
})
export class BooksListComponent {
readonly store = inject(BooksStore);
constructor() {
this.store.loadBooks();
}
}Немає управління підписками. Немає async pipes. Сигнали інтегруються з zoneless change detection Angular для оптимальної продуктивності. З стратегією OnPush change detection за замовчуванням в Angular 22, компоненти Signal Store отримують вигоду від точних, гранулярних оновлень.
Продуктивність та Розмір Бандлу
Легша архітектура Signal Store перетворюється на менші бандли. Основний пакет @ngrx/signals додає приблизно 4KB gzip, порівняно з 15-20KB для повного стеку класичного NgRx (@ngrx/store, @ngrx/effects, @ngrx/entity).
Signal Store також отримує вигоду від change detection Angular на основі сигналів. Computed signals перераховуються лише коли змінюються залежності, а компоненти перерендерюються лише коли оновлюються споживані сигнали. Ця гранулярна реактивність усуває непотрібні цикли change detection, типові для патернів на основі Observable.
Для застосунків, що вже інтенсивно використовують RxJS, селектори класичного NgRx на основі Observable безперешкодно інтегруються з існуючими конвеєрами.
NgRx 22 вимагає Angular 22.x, TypeScript 6.0.x та RxJS 6.5.x або 7.5.x. Оновлення через ng update @ngrx/signals@22 для проектів Signal Store.
Стратегія Міграції з Класичного до Signal Store
Міграція існуючого застосунку класичного NgRx не вимагає повного переписування. Обидва рішення можуть співіснувати, дозволяючи поступове впровадження.
- Почати нові фічі з використанням Signal Store
- Мігрувати ізольовані модулі фіч по одному
- Залишити складні фічі з великою кількістю ефектів на класичному NgRx, якщо вартість міграції перевищує користь
- Використовувати утиліти NgRx Signals interop для з'єднання стану на основі Observable та стану на основі сигналів
Класичний NgRx Store та Signal Store можуть працювати поруч в одному застосунку. Команда NgRx офіційно підтримує обидва підходи та надає утиліти для взаємодії між ними.
Коли Обирати Кожен Підхід
Обирайте Signal Store коли:
- Створення нових застосунків Angular 22+
- Управління станом на рівні фіч або компонентів
- Інтеграція з Resource API Angular та Signal Forms
- Пріоритетом є developer experience та зменшення бойлерплейту
- Команда має обмежений досвід з NgRx
- Важливий розмір бандлу
Обирайте класичний NgRx коли:
- Підтримка існуючих застосунків NgRx
- Потрібні суворі логи екшенів та аудиторські сліди
- Створення складних робочих процесів з розширеними побічними ефектами
- Потрібне повне time-travel налагодження Redux DevTools
- Команда має глибоку експертизу NgRx
Розуміння основ управління станом в обох підходах готує розробників до роботи з різноманітними кодовими базами Angular.
Джерела
- NgRx v22 Release Announcement - Розширення ресурсів, динамічні deep signals, SignalStoreFeatureType
- Angular 22 Feature Overview - Signal Forms стабільні, OnPush за замовчуванням, Resource API
- Using Angular's Resource API with NgRx Signal Store - патерни withProps, інтеграція httpResource
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Signal Store vs Класичний NgRx: Ключові Висновки
- Signal Store зменшує бойлерплейт управління станом на 60-70% порівняно з класичним NgRx
- NgRx 22 додає розширення ресурсів (
withPreviousValueOnLoading,withValueOnError) для плавної обробки асинхронного стану - Функція
withPropsцентралізує ін'єкцію сервісів, усуваючи повторювані списки параметрів між фічами watchStateзабезпечує синхронне відстеження стану для undo/redo та історії стану- Класичний NgRx залишається кращим вибором для корпоративних застосунків, що вимагають суворих аудиторських слідів та складних побічних ефектів
- Обидва підходи можуть співіснувати в одному застосунку, дозволяючи поступову міграцію
- Для нових проектів Angular 22+ Signal Store має бути вибором за замовчуванням, якщо специфічні вимоги не потребують класичного NgRx
Чи знайдеш ти помилку в Angular?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 9 жовтня 2026 р.
Теги
Поділитися
Пов'язані статті

RxJS в Angular 2026: оператори, Subject'и та interop із Signals
RxJS в Angular 2026: опануйте оператори, Subject'и та патерни interop із Signals, які розробники використовують у продакшені, плюс найпоширеніші питання співбесід.

Angular Resource API та httpResource: Повний посібник (Стабільно з Angular 22)
Resource API та httpResource в Angular тепер стабільні починаючи з Angular 22. Цей посібник охоплює resource(), rxResource(), httpResource(), новий helper chain() для залежних ресурсів, валідацію Zod та питання для співбесід.

Просунутий Dependency Injection в Angular 2026: Providers, Tokens та Питання на Співбесідах
Комплексний посібник з просунутих механізмів Dependency Injection в Angular. Providers, InjectionToken, ієрархія інжекторів та найпоширеніші питання на технічних співбесідах.