
Guía Práctica de NgRx ComponentStore: Manejo de Estados Locales/de Componente
Tabla de Contenidos
Tabla de Contenidos
Actualización (2026): Ya no recomiendo ComponentStore para código nuevo. NgRx sugiere empezar con SignalStore, y es lo que uso hoy; puedes verlo en acción en NgRx SignalStore Events. Este post sigue siendo útil si mantienes una app que usa ComponentStore.
En esta serie sobre ComponentStore, compartiré mi experiencia usándolo durante aproximadamente un año. Empezaremos con lo básico: qué es y cómo usarlo, y poco a poco profundizaremos en patrones y buenas prácticas.
La definición oficial de ComponentStore del sitio oficial de NgRx:
ComponentStore is a stand-alone library that helps to manage local/component state. It's an alternative to reactive push-based "Service with a Subject" approach.
En palabras más simples: ComponentStore es una librería dedicada a manejar estados locales/de componente. Nos da una metodología consistente para inicializar, leer, actualizar y manejar side effects en nuestro store.
Crearé un ejemplo real en GitHub con todo el código funcionando: una app sencilla para explorar series de TV usando https://www.tvmaze.com [Doc de la API].
Qué nos aporta ComponentStore
La idea central de un store es servir como única fuente de verdad: un lugar centralizado donde los datos siempre son correctos y están actualizados. Esto asegura la consistencia en toda nuestra aplicación.
Desde esta perspectiva, ComponentStore ofrece métodos para inicializar el store (lo cual se puede hacer de forma lazy), actualizarlo, reemplazar parte de su contenido (patch) y manejar effects (side effects).
En resumen, ComponentStore nos permite usar estos métodos:
setState()patchState()select()updater()effect()
Además, tiene algunos lifecycle hooks para ejecutar tareas después de que el ComponentStore se instancia.
Muy bien, vamos al grano. Este es el repo si quieres seguir todo el código.
Inicialización del store
- Podemos hacerlo a través del constructor del store (mi forma favorita),
// series.store.ts
export interface SeriesState {
series: Serie[];
selectedId: number;
}
@Injectable()
export class SeriesStore extends ComponentStore<SeriesState> {
constructor() {
super({series: []}); // <--- Inicialización
}
}
- O usando
setState(la forma lazy, desde el componente)
// series.component.ts
ngOnInit() {
this.store.setState({ series: [] }); // <--- Inicialización
}
}
Leer del store
Para obtener o leer un valor usamos el método select y creamos un selector para usar en nuestros componentes:
// series.store.ts
export interface SeriesState {
series: Serie[]; // <- el selector obtiene esta parte del estado
selectedId: number;
}
@Injectable()
export class SeriesStore extends ComponentStore<SeriesState> {
// leemos un array de series
readonly series$ = this.select((state) => state.series);
constructor() {
super(initialState);
}
}
Se puede combinar con otros selectores u observables (muy útil para crear un view model)
// series.store.ts
// código omitido ...
// observable creado con el array de series
readonly series$ = this.select((state) => state.series);
// observable creado con el selectedId
readonly selectedSerieId$ = this.select((state) => state.selectedId);
// nuevo selector combinando observables
readonly selectedSerie$ = this.select(
this.series$,
this.selectedSerieId$,
(series, id) => series.find((serie) => serie.id === id);
// código omitido ...
A partir de Angular 16 podemos usar signal, y component-store nos permite convertir observables en signals usando selectSignal como selector. Para ver cómo usarlo, revisa la rama use-signal-with-component-store. Usaremos este enfoque en los próximos posts.
Actualizar el store
Necesitamos una forma de actualizar el store. ComponentStore nos da 3 maneras de hacerlo: usando setState, patchState o creando un updater (una de mis formas favoritas):
setState(): sirve para reemplazar el estado por completo o aplicar una función que recibe el estado anterior y devuelve uno nuevo.
this.setState(state => ({ ...state, data: newData, loading: false }));
patchState(): actualización parcial del store. Es útil cuando solo necesitas cambiar una o pocas propiedades del estado sin tocar el resto.
this.patchState({ state: 'loading' });
updater(): permite actualizar el store enviándole un valor de entrada.
// definición del updater
readonly selectedSerieId = this.updater((state, selectedId: number) => {
return { ...state, selectedId };
});
// uso del updater en el componente
selectedMovie(selectedId: number) {
this.store.selectedSerieId(selectedId); // <- el updater actúa como un método con un parámetro
}
Effects
Los effects en ComponentStore son herramientas que separan de los componentes las acciones con side effects (como las llamadas HTTP), para que los componentes solo se encarguen de mostrar datos y disparar actualizaciones.
// Updater: agrega series al store
readonly addSeries = this.updater((state, series: Serie[]) => {
return { ...state, series, state: 'loaded' };
});
// Effect
// Obtiene los datos de forma asíncrona y usa el updater para
// agregar el resultado al store.
readonly getAllSeries = this.effect<void>((trigger$) =>
trigger$.pipe(
// Marca el estado del componente como cargando
tap(() => this.patchState({ state: 'loading' })),
switchMap(() =>
this.seriesService.getSeries().pipe(
tapResponse({
// Si la petición tiene éxito, actualiza el store
next: (movies) => this.addSeries(movies),
error: (e: HttpErrorResponse) => {
// Si la petición falla, actualiza el store con el estado de error
this.patchState({ state: 'error' });
this.handleError(e);
},
})
);
Los effects reciben 0 o 1 parámetro. Ese parámetro puede ser un valor simple o un objeto con la estructura necesaria para hacer la petición.
Un ejemplo con parámetro:
readonly getSerieDetail = this.effect((serieId$: Observable<string>) => {
return serieId$.pipe(
// Elige el operador de aplanamiento adecuado.
switchMap((id) => this.seriesService.fetchSerie(id).pipe(
// código omitido
)),
);
});
Usando el store en nuestro componente
Nuestro componente,
@Component({
selector: 'app-series',
standalone: true,
imports: [AsyncPipe, NgForOf, NgIf],
templateUrl: './series.component.html',
styleUrls: ['./series.component.scss'],
providers: [SeriesStore, SeriesService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SeriesComponent implements OnInit {
private readonly store = inject(SeriesStore);
readonly vm$: Observable<ViewModelComponent> = this.store.vm$; // Nuestro ViewModel expuesto a la plantilla
ngOnInit(): void {
this.store.getAllSeries();
}
}
y nuestra plantilla:
<ng-container *ngIf="vm$ | async as vm">
<h1>Series</h1>
<!--Se muestra mientras carga:-->
<ng-container *ngIf="vm.isLoading; else contentTpl">
<p>Loading...</p>
</ng-container>
<!--Plantilla cuando los datos están cargados:-->
<ng-template #contentTpl>
<ul *ngIf="vm.series.length; else noItemsTpl">
<li *ngFor="let serie of vm.series">
{{ serie.name }}
</li>
</ul>
<!-- Si no hay series, muestra esto:-->
<ng-template #noItemsTpl>
<p>No series found</p>
</ng-template>
</ng-template>
</ng-container>
Recomendaciones
En mi experiencia, siempre uso un View Model para mostrar la información final que se renderiza en la plantilla. Casi cualquier if de la plantilla se puede convertir en un selector y exponerse en el View Model.
El Store normalmente contiene todos los datos crudos que uso, el estado del componente y cualquier dato sin procesar. Todos esos datos, ya sea que impliquen cálculos, filtros o selecciones, se transforman en un selector o en una combinación de selectores que se expone en el View Model.
Conclusión
NgRx ComponentStore es una herramienta útil para manejar estados locales/de componente en una app. Permite inicializar, actualizar y manejar side effects en el store, haciendo que el manejo de estado sea consistente y eficiente. Métodos como setState(), patchState(), select(), updater() y effect() dan un control flexible y completo del store. Su integración con los lifecycle hooks lo hace aún más versátil. Usando bien estas herramientas, puedes mantener los datos consistentes en tus apps, con mejor rendimiento y experiencia de usuario.
¿Tienes alguna experiencia con ComponentStore que quieras compartir? ¿O alguna pregunta sobre lo que vimos? Me encantaría leer tus comentarios y conversarlo en la sección de abajo.


