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 vs Classic NgRx comparison

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.

Szybki przewodnik decyzyjny

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.

AspektKlasyczny NgRxSignal Store
Model mentalnyRedux/FluxReaktywne serwisy
BoilerplateWysoki (akcje, reducery, selektory, efekty)Niski (stan, metody, computed)
ReaktywnośćRxJS ObservablesAngular Signals
Resource APIRęczna integracjaNatywna z @ngrx/signals/resource
DevToolsPełne Redux DevToolsSignal Store DevTools (plugin)
Krzywa uczeniaStromaUmiarkowana
Rozmiar bundleWię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.

books.actions.tstypescript
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.

books.reducer.tstypescript
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.

books.selectors.tstypescript
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."

books.store.tstypescript
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.

books-resource.store.tstypescript
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.

books-entity.store.tstypescript
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.

with-request-status.tstypescript
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.

books-with-status.store.tstypescript
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.

state-history.tstypescript
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.

books-list.component.tstypescript
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.

Wymagania NgRx 22

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

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 withProps centralizuje wstrzykiwanie serwisów, eliminując powtarzane listy parametrów między feature'ami
  • watchState umoż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
Wyzwanie dnia

Znajdziesz błąd w Angular?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Zał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

#angular
#ngrx
#state-management
#signals
#typescript

Udostępnij

Powiązane artykuły