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 i rekomendacje na rok 2026.

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. To porównanie analizuje oba rozwiązania, aby pomóc programistom Angular podjąć świadome decyzje architektoniczne w 2026 roku.
Należy używać Signal Store dla stanu na poziomie feature'ów i aplikacji o średniej złożoności, gdzie redukcja boilerplate'u ma znaczenie. 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 | | DevTools | Pełne Redux DevTools | Signal Store DevTools (plugin) | | Krzywa uczenia | Stroma | Umiarkowana | | Rozmiar bundle | Większy | Mniejszy |
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.
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.
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState } 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'),
}))
);
}
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.
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.
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.
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 20+
- Zarządzanie stanem na poziomie feature'ów lub komponentów
- 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.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Podsumowanie
- Signal Store redukuje boilerplate zarządzania stanem o 60-70% w porównaniu do klasycznego NgRx
- 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ę
- Computed signals w Signal Store zapewniają automatyczną memoizację bez jawnych definicji selektorów
- Własne feature'y store (
signalStoreFeature) umożliwiają reużywalne cross-cutting concerns - Dla nowych projektów Angular 20+ w 2026 roku Signal Store powinien być domyślnym wyborem, chyba że specyficzne wymagania wymagają klasycznego NgRx
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 20 Resource API i httpResource: Kompletny przewodnik z pytaniami rekrutacyjnymi
Szczegolowy przewodnik po Resource API w Angular 20: resource(), rxResource() i httpResource() do reaktywnego pobierania danych. Walidacja Zod, sledzenie statusu ResourceStatus, migracja z HttpClient oraz pytania na rozmowy kwalifikacyjne Angular 2026.

Pytania rekrutacyjne Angular 19: Signals, SSR i niezbędne koncepcje
Najczęstsze pytania rekrutacyjne Angular 19: Signals, inkrementalne hydration, zoneless change detection oraz nowe API reaktywne wraz z przykładami kodu i oczekiwanymi odpowiedziami.