NgRx Signal Store vs Klasyczny NgRx w 2026: Który Wybrać?
Kompleksowe porównanie NgRx Signal Store i klasycznego NgRx do zarządzania stanem w Angular. Praktyczne przykłady kodu z NgRx 22 i rekomendacje.

NgRx Signal Store reprezentuje fundamentalną zmianę w zarządzaniu stanem Angular, zastępując tradycyjny wzorzec Redux (akcje-reducery-selektory) reaktywnym podejściem opartym na sygnałach. Wraz z NgRx 22 (wydanym w sierpniu 2026), Signal Store zyskuje rozszerzenia zasobów, dynamiczne deep signals oraz ulepszoną kompozycyjność. To porównanie analizuje oba rozwiązania, aby pomóc programistom Angular podjąć świadome decyzje architektoniczne.
Należy używać Signal Store dla stanu na poziomie feature'ów, aplikacji o średniej złożoności oraz integracji z natywnym Resource API Angular. Klasyczny NgRx sprawdzi się w aplikacjach enterprise wymagających ścisłych ścieżek audytu, złożonych efektów ubocznych i rozbudowanego debugowania w DevTools.
Zrozumienie Różnic Architektonicznych
Klasyczny NgRx opiera się na wzorcu Redux: akcje opisują zdarzenia, reducery produkują nowy niemutowalny stan, a selektory wydobywają dane. Każda zmiana stanu przepływa przez przewidywalny, możliwy do śledzenia pipeline.
Signal Store przyjmuje inne podejście. Zamiast dispatch'owania akcji przez reducery, stan żyje w reaktywnych sygnałach. Aktualizacje odbywają się przez metody wywołujące patchState(), a wartości pochodne pochodzą z computed signals zamiast memoizowanych selektorów.
| Aspekt | Klasyczny NgRx | Signal Store |
|---|---|---|
| Model mentalny | Redux/Flux | Reaktywne serwisy |
| Boilerplate | Wysoki (akcje, reducery, selektory, efekty) | Niski (stan, metody, computed) |
| Reaktywność | RxJS Observables | Angular Signals |
| Resource API | Ręczna integracja | Natywna z @ngrx/signals/resource |
| DevTools | Pełne Redux DevTools | Signal Store DevTools (plugin) |
| Krzywa uczenia | Stroma | Umiarkowana |
| Rozmiar bundle | Większy (~15-20KB gzip) | Mniejszy (~4KB gzip) |
Dla programistów znających już Angular Signals, Signal Store wydaje się naturalnym rozszerzeniem reaktywnych prymitywów frameworka.
Klasyczny NgRx: Wzorzec Redux w Angular
Klasyczny NgRx organizuje zarządzanie stanem wokół akcji, reducerów, selektorów i efektów. Ta separacja wymusza przewidywalność kosztem wielosłowności.
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 grupuje powiązane akcje pod jednym źródłem, poprawiając czytelność w DevTools i redukując boilerplate w porównaniu do pojedynczych wywołań 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,
}))
);Reducery pozostają czystymi funkcjami zwracającymi nowe obiekty stanu. Funkcja on() obsługuje dopasowywanie akcji i aktualizacje stanu w zwięzły sposób.
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)
);Selektory automatycznie zapewniają memoizację. Selektor selectPublishedBooks przelicza się tylko gdy tablica książek się zmienia, czyniąc odczyty wydajnymi nawet w często aktualizowanych store'ach.
NgRx Signal Store: Zarządzanie Stanem Oparte na Sygnałach
Signal Store konsoliduje stan, wartości computed i metody w pojedynczym injektowalnym serwisie. Oficjalna dokumentacja NgRx opisuje go jako "w pełni funkcjonalne rozwiązanie do zarządzania stanem z natywnym wsparciem dla 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),
}));
},
}))
);Całe zarządzanie stanem feature'a mieści się w jednym pliku. Właściwości stanu automatycznie stają się sygnałami, computed signals zastępują selektory, a metody zastępują dispatch'owanie akcji.
NgRx 22: Rozszerzenia Zasobów i withProps
NgRx 22 wprowadza punkt wejścia @ngrx/signals/resource, umożliwiający bezpośrednią integrację z Resource API Angular. Funkcja withProps centralizuje wstrzykiwanie serwisów, a rozszerzenia zasobów obsługują typowe wzorce asynchroniczne.
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' },
// Wstrzykiwanie serwisów przez withProps (prywatne z konwencji z podkreśleniem)
withProps(() => ({
_booksService: inject(BooksService),
})),
// Konfiguracja zasobów z rozszerzeniami
withProps((store) => ({
_booksResource: extendResource(
httpResource<Book[]>({
url: '/api/books',
defaultValue: [],
}),
withPreviousValueOnLoading(), // Zachowuje ostatnią wartość podczas przeładowania
withValueOnError([]) // Zwraca pustą tablicę w przypadku błędu
),
})),
// Eksponowanie zasobu tylko do odczytu dla konsumentów
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)
),
}))
);Rozszerzenia zasobów rozwiązują dwa typowe problemy: migotanie podczas przeładowywania (naprawione przez withPreviousValueOnLoading) oraz obsługę stanu błędu (naprawione przez withValueOnError lub withPreviousValueOnError). Te rozszerzenia komponują się i mogą być dostarczane globalnie przez provideResourceExtensions.
Zarządzanie Encjami z withEntities
Oba podejścia wspierają kolekcje encji, ale funkcja withEntities w Signal Store drastycznie redukuje ilość kodu.
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));
},
}))
);Funkcja withEntities automatycznie dostarcza sygnały ids, entityMap i entities. Updatery encji jak addEntity, removeEntity i updateEntity obsługują mutacje kolekcji.
Gotowy na rozmowy o Angular?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Własne Feature'y Store dla Reużywalności
Funkcja signalStoreFeature w Signal Store umożliwia ekstrakcję i ponowne użycie cross-cutting concerns w różnych store'ach. NgRx 22 dodaje SignalStoreFeatureType do uproszczenia ekstrakcji typów.
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: ekstrakcja typów bez ręcznej redeklaracji
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' };
}Ten własny feature może być teraz skomponowany w dowolnym store'ze wymagającym śledzenia stanu ładowania.
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());
}
},
}))
);Skomponowany store teraz eksponuje computed signals isPending(), isFulfilled() i isError() bez duplikowania logiki.
Synchroniczne Śledzenie Stanu z watchState
NgRx 22 dodaje funkcję watchState do synchronicznego śledzenia zmian stanu, przydatną dla funkcjonalności undo/redo lub historii stanu.
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[] = [];
// Synchroniczne śledzenie: przechwytuje każdą zmianę stanu, bez łączenia
watchState(store, (state) => {
history.push(structuredClone(state));
});
return { history };
}W przeciwieństwie do śledzenia opartego na efektach, które łączy aktualizacje, watchState uruchamia się synchronicznie przy każdym wywołaniu patchState, przechwytując pełną historię stanu.
Wzorce Integracji z Komponentami
Signal Store naturalnie integruje się ze składnią szablonów Angular poprzez odczyty sygnałów.
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">Ładowanie książek...</div>
}
@if (store.isError()) {
<div class="error-message">Nie udało się załadować książek</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)">Usuń</button>
</div>
}
<p>Razem: {{ store.totalBooks() }} książek</p>
`,
})
export class BooksListComponent {
readonly store = inject(BooksStore);
constructor() {
this.store.loadBooks();
}
}Brak zarządzania subskrypcjami. Brak async pipe'ów. Sygnały integrują się z zoneless change detection w Angular dla optymalnej wydajności. Z domyślną strategią OnPush change detection w Angular 22, komponenty Signal Store korzystają z precyzyjnych, granularnych aktualizacji.
Wydajność i Rozmiar Bundle
Lżejsza architektura Signal Store przekłada się na mniejsze bundle. Podstawowy pakiet @ngrx/signals dodaje około 4KB po gzip, w porównaniu do 15-20KB dla pełnego stosu klasycznego NgRx (@ngrx/store, @ngrx/effects, @ngrx/entity).
Signal Store korzysta również z opartego na sygnałach change detection w Angular. Computed signals przeliczają się tylko gdy zależności się zmieniają, a komponenty re-renderują się tylko gdy konsumowane sygnały się aktualizują. Ta granularna reaktywność eliminuje niepotrzebne cykle change detection typowe dla wzorców opartych na Observable.
Dla aplikacji już intensywnie używających RxJS, oparte na Observable selektory klasycznego NgRx bezproblemowo integrują się z istniejącymi pipeline'ami.
NgRx 22 wymaga Angular 22.x, TypeScript 6.0.x oraz RxJS 6.5.x lub 7.5.x. Aktualizacja przez ng update @ngrx/signals@22 dla projektów Signal Store.
Strategia Migracji z Klasycznego do Signal Store
Migracja istniejącej aplikacji klasycznego NgRx nie wymaga przepisania wszystkiego naraz. Oba rozwiązania mogą współistnieć, umożliwiając stopniową adopcję.
- Rozpoczęcie nowych feature'ów z użyciem Signal Store
- Migracja izolowanych modułów feature'ów pojedynczo
- Pozostawienie złożonych feature'ów z wieloma efektami na klasycznym NgRx, jeśli koszt migracji przewyższa korzyści
- Użycie narzędzi interoperacyjności NgRx Signals do łączenia stanu opartego na Observable ze stanem opartym na sygnałach
Klasyczny NgRx Store i Signal Store mogą działać obok siebie w tej samej aplikacji. Zespół NgRx oficjalnie wspiera oba podejścia i dostarcza narzędzia interoperacyjności do łączenia ich.
Kiedy Wybrać Które Podejście
Wybierz Signal Store gdy:
- Budowanie nowych aplikacji Angular 22+
- Zarządzanie stanem na poziomie feature'ów lub komponentów
- Integracja z Resource API Angular i Signal Forms
- Priorytetem jest developer experience i redukcja boilerplate'u
- Zespół ma ograniczone doświadczenie z NgRx
- Rozmiar bundle ma znaczenie
Wybierz klasyczny NgRx gdy:
- Utrzymywanie istniejących aplikacji NgRx
- Wymagane są ścisłe ścieżki logowania akcji i audytu
- Budowanie złożonych workflow'ów z rozbudowanymi efektami ubocznymi
- Potrzeba pełnego time-travel debugging w Redux DevTools
- Zespół ma głęboką ekspertyzę NgRx
Zrozumienie podstaw zarządzania stanem w obu podejściach przygotowuje programistów do pracy z różnorodnymi bazami kodu Angular.
Źródła
- NgRx v22 Release Announcement - Rozszerzenia zasobów, dynamiczne deep signals, SignalStoreFeatureType
- Angular 22 Feature Overview - Signal Forms stabilne, OnPush domyślne, Resource API
- Using Angular's Resource API with NgRx Signal Store - wzorce withProps, integracja httpResource
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Signal Store vs Klasyczny NgRx: Najważniejsze Wnioski
- Signal Store redukuje boilerplate zarządzania stanem o 60-70% w porównaniu do klasycznego NgRx
- NgRx 22 dodaje rozszerzenia zasobów (
withPreviousValueOnLoading,withValueOnError) dla płynnej obsługi stanu asynchronicznego - Funkcja
withPropscentralizuje wstrzykiwanie serwisów, eliminując powtarzane listy parametrów między feature'ami watchStateumożliwia synchroniczne śledzenie stanu dla undo/redo i historii stanu- Klasyczny NgRx pozostaje lepszym wyborem dla aplikacji enterprise wymagających ścisłych ścieżek audytu i złożonych efektów ubocznych
- Oba podejścia mogą współistnieć w tej samej aplikacji, umożliwiając stopniową migrację
- Dla nowych projektów Angular 22+ Signal Store powinien być domyślnym wyborem, chyba że specyficzne wymagania wymagają klasycznego NgRx
Znajdziesz błąd w Angular?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 9 października 2026
Tagi
Udostępnij
Powiązane artykuły

RxJS w Angularze 2026: operatory, Subjecty i interop z Signals
RxJS w Angularze 2026: opanuj operatory, Subjecty i wzorce interoperacyjności z Signals używane na produkcji oraz pytania rekrutacyjne, które padają najczęściej.

Angular Resource API i httpResource: Kompletny przewodnik (Stabilne w Angular 22)
Resource API i httpResource w Angular sa teraz stabilne od Angular 22. Przewodnik obejmuje resource(), rxResource(), httpResource(), nowy helper chain() do zaleznych zasobow, walidacje Zod i pytania rekrutacyjne.

Zaawansowany Dependency Injection w Angular 2026: Providers, Tokens i Pytania Rekrutacyjne
Kompleksowy przewodnik po zaawansowanych mechanizmach Dependency Injection w Angular. Providers, InjectionToken, hierarchia injectorów oraz najczęstsze pytania na rozmowach kwalifikacyjnych.