CodeForge

Angular 21 de Cero a Experto / Estado a gran escala

NgRx SignalStore: stores estructurados con señales

Teoría24 min20 XP

Cuando tu app tiene muchos stores, escribir a mano el patrón "estado privado + lecturas + derivados + acciones" una y otra vez se vuelve repetitivo. NgRx SignalStore empaqueta ese patrón en una API declarativa y componible: withState, withComputed, withMethods. Es la recomendación moderna de NgRx —construida sobre señales, no sobre Observables—. Hoy la conoces y construyes un mini-SignalStore para entender su elegancia por dentro.

Bloques que se ensamblan

Un SignalStore básico

bolsillo.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

interface Gasto { id: number; nombre: string; valor: number; }
interface BolsilloState { gastos: Gasto[]; presupuesto: number; }

export const BolsilloStore = signalStore(
{ providedIn: 'root' },   // se inyecta como cualquier servicio

// 1. el estado inicial (se expone como señales de solo lectura)
withState<BolsilloState>({ gastos: [], presupuesto: 500000 }),

// 2. derivados (computed) a partir del estado
withComputed((store) => ({
  total: computed(() => store.gastos().reduce((s, g) => s + g.valor, 0)),
  saldo: computed(() => store.presupuesto() - store.gastos().reduce((s, g) => s + g.valor, 0)),
})),

// 3. métodos (acciones) que actualizan el estado con patchState
withMethods((store) => ({
  agregar(nombre: string, valor: number) {
    patchState(store, {
      gastos: [...store.gastos(), { id: Date.now(), nombre, valor }],
    });
  },
  eliminar(id: number) {
    patchState(store, { gastos: store.gastos().filter((g) => g.id !== id) });
  },
})),
);

Reconoces las cuatro capas de tu BolsilloStore del módulo 3 —estado, lecturas, derivados, acciones— pero ahora estructuradas con bloques estándar. El componente lo consume igual: inject(BolsilloStore), y lee store.total(), llama store.agregar(...). patchState es la forma controlada de actualizar (actualiza solo las claves que le pasas, inmutablemente).

withEntities: CRUD de colecciones gratis

El patrón más repetitivo es manejar una colección de entidades (agregar, actualizar, eliminar por id). withEntities lo automatiza:

entities.ts
import { withEntities, setAllEntities, addEntity, removeEntity } from '@ngrx/signals/entities';

export const GastosStore = signalStore(
{ providedIn: 'root' },
withEntities<Gasto>(),   // te da entities() (la lista) y utilidades CRUD
withMethods((store) => ({
  cargar(gastos: Gasto[]) { patchState(store, setAllEntities(gastos)); },
  agregar(g: Gasto) { patchState(store, addEntity(g)); },
  quitar(id: number) { patchState(store, removeEntity(id)); },
})),
);
// store.entities() → la lista; el CRUD por id ya viene resuelto

En vez de escribir el filter/map/spread de cada operación, withEntities te da un almacén de entidades con helpers probados. Menos código repetido, menos bugs.

Antes (NgRx clásico) vs Ahora (SignalStore)

Constrúyelo: un mini-SignalStore en vivo

La magia de signalStore es la COMPOSICIÓN: cada with* recibe el store parcial y le añade propiedades. Aquí, una versión mínima que compone estado + derivados + métodos, en vivo:

Observa la idea central: signalStore es un reduce que pasa el store parcial por cada bloque with*, y cada bloque le AÑADE propiedades (señales de estado, derivados, métodos). Esa composición es lo que hace a SignalStore extensible: withEntities es solo otro bloque que añade una colección y sus helpers. Ahora, cuando leas un SignalStore real, verás la estructura que acabas de construir.

¿Qué ventaja principal ofrece NgRx SignalStore frente al NgRx clásico (Store/Actions/Reducers)?

Mini-reto

En el playground, amplía tu mini-SignalStore: 1) añade al withState un campo filtro (string); 2) un withComputed gastosVisibles que filtre según filtro; 3) un método filtrarPor(cat) que use patchState. Prueba agregar y filtrar. Luego escribe cómo se vería con el signalStore real de @ngrx/signals.

Qué sigue

Tienes la herramienta para stores estructurados. Pero una app grande no es un store gigante: es muchas FEATURES, cada una con su estado, componentes y servicios, bien delimitadas. La próxima lección es la arquitectura por features —cómo organizar los archivos de una app que vive años—, conectando con lo que viste en React (SC-06 M10).