NgRx Signal Store vs NgRx classico nel 2026: quale scegliere per il tuo progetto Angular?

Confronto approfondito tra NgRx Signal Store e NgRx classico nel 2026. Architettura, performance, boilerplate e strategie di migrazione per la gestione dello stato in Angular.

NgRx Signal Store vs NgRx classico nel 2026: quale scegliere per il tuo progetto Angular?

NgRx Signal Store rappresenta un cambiamento fondamentale nella gestione dello stato Angular, sostituendo il tradizionale pattern ispirato a Redux basato su actions, reducers e selectors con un approccio reattivo basato sui signals. Questo confronto esamina entrambe le soluzioni per aiutare gli sviluppatori Angular a prendere decisioni architetturali informate nel 2026.

Guida rapida alla scelta

Utilizzare Signal Store per la gestione dello stato a livello di feature e applicazioni di media complessità dove la riduzione del boilerplate è importante. Utilizzare NgRx classico per applicazioni enterprise che richiedono audit trail rigorosi, side effects complessi e debugging avanzato con DevTools.

Comprendere le differenze architetturali

NgRx classico segue il pattern Redux: le actions descrivono eventi, i reducers producono nuovo stato immutabile, e i selectors estraggono dati. Ogni cambiamento di stato passa attraverso una pipeline prevedibile e tracciabile.

Signal Store adotta un approccio diverso. Invece di inviare actions attraverso reducers, lo stato risiede in signals reattivi. Gli aggiornamenti avvengono tramite metodi che chiamano patchState(), e i valori derivati provengono da computed signals invece che da selectors memorizzati.

| Aspetto | NgRx classico | Signal Store | |---------|---------------|---------------| | Modello mentale | Redux/Flux | Servizi reattivi | | Boilerplate | Alto (actions, reducers, selectors, effects) | Basso (state, metodi, computed) | | Reattività | RxJS Observables | Angular Signals | | DevTools | Redux DevTools completi | Signal Store DevTools (via plugin) | | Curva di apprendimento | Ripida | Moderata | | Dimensione bundle | Maggiore | Minore |

Per gli sviluppatori già familiari con Angular Signals, Signal Store risulta un'estensione naturale delle primitive reattive del framework.

NgRx classico: il pattern Redux in Angular

NgRx classico organizza la gestione dello stato attorno ad actions, reducers, selectors ed effects. Questa separazione garantisce prevedibilità a costo di maggiore verbosità.

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 }>(),
  },
});

L'API createActionGroup raggruppa actions correlate sotto un'unica source, migliorando la leggibilità nei DevTools e riducendo il boilerplate rispetto alle chiamate individuali di 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,
  }))
);

I reducers rimangono funzioni pure che restituiscono nuovi oggetti stato. La funzione on() gestisce il matching delle actions e gli aggiornamenti dello stato in modo conciso.

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)
);

I selectors forniscono la memoizzazione automaticamente. Il selector selectPublishedBooks ricalcola solo quando l'array books cambia, rendendo le letture efficienti anche in store frequentemente aggiornati.

NgRx Signal Store: gestione dello stato signals-first

Signal Store consolida stato, valori computed e metodi in un unico servizio iniettabile. La documentazione ufficiale NgRx lo descrive come una "soluzione completa per la gestione dello stato con supporto nativo per 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),
      }));
    },
  }))
);

L'intera gestione dello stato di una feature sta in un unico file. Le proprietà dello stato diventano automaticamente signals, i computed signals sostituiscono i selectors, e i metodi sostituiscono il dispatch delle actions.

Gestione delle entità con withEntities

Entrambi gli approcci supportano le collezioni di entità, ma la feature withEntities di Signal Store riduce drasticamente il codice.

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));
    },
  }))
);

La feature withEntities fornisce automaticamente i signals ids, entityMap e entities. Gli updater per entità come addEntity, removeEntity e updateEntity gestiscono le mutazioni della collezione.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Custom Store Features per la riusabilità

La funzione signalStoreFeature di Signal Store permette di estrarre logiche trasversali e riutilizzarle tra diversi store.

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' };
}

Questa feature custom può ora essere composta in qualsiasi store che necessita del tracciamento dello stato di caricamento.

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());
      }
    },
  }))
);

Lo store composto ora espone i computed signals isPending(), isFulfilled() e isError() senza duplicare la logica.

Pattern di integrazione nei componenti

Signal Store si integra naturalmente con la sintassi dei template Angular attraverso la lettura dei signals.

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">Loading books...</div>
    }

    @if (store.isError()) {
      <div class="error-message">Failed to load books</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)">Remove</button>
      </div>
    }

    <p>Total: {{ store.totalBooks() }} books</p>
  `,
})
export class BooksListComponent {
  readonly store = inject(BooksStore);

  constructor() {
    this.store.loadBooks();
  }
}

Nessuna gestione delle subscription necessaria. Nessun async pipe. I signals si integrano con la change detection zoneless di Angular per prestazioni ottimali.

Performance e dimensione del bundle

L'architettura più leggera di Signal Store si traduce in bundle più piccoli. Il package core @ngrx/signals aggiunge circa 4KB gzipped, rispetto ai 15-20KB dello stack NgRx classico completo (@ngrx/store, @ngrx/effects, @ngrx/entity).

Signal Store beneficia inoltre della change detection basata sui signals di Angular. I computed signals ricalcolano solo quando le dipendenze cambiano, e i componenti ri-renderizzano solo quando i signals consumati si aggiornano. Questa reattività granulare elimina cicli di change detection non necessari comuni nei pattern basati su Observable.

Per applicazioni che utilizzano già intensivamente RxJS, i selectors basati su Observable di NgRx classico si integrano perfettamente con le pipeline esistenti.

Strategia di migrazione da classico a Signal Store

Migrare un'applicazione NgRx classica esistente non richiede una riscrittura totale. Entrambe le soluzioni possono coesistere, permettendo un'adozione incrementale.

  • Iniziare con nuove features usando Signal Store
  • Migrare i moduli feature isolati uno alla volta
  • Mantenere features complesse con molti effects su NgRx classico se il costo di migrazione supera i benefici
  • Utilizzare le utility di interoperabilità NgRx Signals per collegare stato basato su Observable e basato su signals
La coesistenza funziona

NgRx Store classico e Signal Store possono funzionare insieme nella stessa applicazione. Il team NgRx supporta ufficialmente entrambi gli approcci e fornisce utility di interoperabilità per collegarli.

Quando scegliere ciascun approccio

Scegliere Signal Store quando:

  • Si sviluppano nuove applicazioni Angular 20+
  • Si gestisce stato a livello di feature o componente
  • La developer experience e la riduzione del boilerplate sono priorità
  • Il team ha esperienza limitata con NgRx
  • La dimensione del bundle è importante

Scegliere NgRx classico quando:

  • Si mantengono applicazioni NgRx esistenti
  • Sono richiesti logging rigoroso delle actions e audit trail
  • Si costruiscono workflow complessi con side effects estesi
  • È necessario il time-travel debugging completo con Redux DevTools
  • Il team possiede profonda esperienza NgRx

Per i fondamenti della gestione dello stato, comprendere entrambi gli approcci prepara gli sviluppatori per codebase Angular diverse.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

  • Signal Store riduce il boilerplate della gestione dello stato del 60-70% rispetto a NgRx classico
  • NgRx classico rimane la scelta migliore per applicazioni enterprise che richiedono audit trail rigorosi e side effects complessi
  • Entrambi gli approcci possono coesistere nella stessa applicazione, abilitando una migrazione incrementale
  • I computed signals di Signal Store forniscono memoizzazione automatica senza definizioni esplicite di selectors
  • Le custom store features (signalStoreFeature) permettono logiche trasversali riutilizzabili
  • Per nuovi progetti Angular 20+ nel 2026, Signal Store dovrebbe essere la scelta predefinita a meno che requisiti specifici non richiedano NgRx classico

Condividi

Articoli correlati