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 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. To porównanie analizuje oba rozwiązania, aby pomóc programistom Angular podjąć świadome decyzje architektoniczne w 2026 roku.

Szybki przewodnik decyzyjny

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.

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.

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.

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

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.

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.

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
Współistnienie działa

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

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

Udostępnij

Powiązane artykuły