Gestión de Estado en Angular con Signals usando ResourceService
Introducción
En Angular moderno, los Signals permiten implementar un store reactivo de manera nativa, sin librerías externas. Un Signal mantiene un valor interno y notifica automáticamente a todos los componentes y funciones que dependen de él cuando cambia. Esto hace que la gestión de estado sea simple, predecible y eficiente, especialmente para aplicaciones que requieren compartir datos entre múltiples componentes.
La ventaja de Signals frente a otros enfoques es que la reactividad está integrada en el framework, sin necesidad de subscribe(), observables intermedios o memorias que limpiar. Esto permite que los componentes solo consuman datos y reaccionen automáticamente a los cambios del estado.
Conceptos Clave
-
Signal: Contenedor de un valor reactivo que puede actualizarse mediante
.set()o.update(). - Computed: Función derivada que calcula valores basados en Signals, actualizándose automáticamente cuando alguno de los Signals que depende cambian.
- Reactividad automática: Los componentes que usan Signals se actualizan sin suscripciones explícitas.
- Inmutabilidad opcional: Aunque los valores pueden mutar, es recomendable actualizar el Signal creando un nuevo estado para mantener consistencia y evitar efectos secundarios.
Store con Signals y ResourceService
import { Injectable, signal, computed } from '@angular/core';
import { ResourceService } from './resource.service';
import { Resource } from './resource.model';
@Injectable({ providedIn: 'root' })
export class ResourceStore {
// Estado principal
private resourcesSignal = signal<Resource[]>([]);
// Selector derivado: número total de recursos
readonly resourceCount = computed(() => this.resourcesSignal().length);
// Selector derivado: filtrar recursos por nombre
filterResources(name: string) {
return computed(() => this.resourcesSignal().filter((r) => r.name.includes(name)));
}
constructor(private resourceService: ResourceService) {}
// Cargar recursos desde el servicio
loadResources() {
const resources = this.resourceService.get(); // Devuelve Resource[]
this.resourcesSignal.set(resources);
}
// Añadir un recurso
addResource(resource: Resource) {
this.resourcesSignal.update((current) => [...current, resource]);
}
// Eliminar un recurso por id
removeResource(id: number) {
this.resourcesSignal.update((current) => current.filter((r) => r.id !== id));
}
// Actualizar un recurso
updateResource(updated: Resource) {
this.resourcesSignal.update((current) =>
current.map((r) => (r.id === updated.id ? updated : r))
);
}
}
Componente que usa la store
import { Component, OnInit } from '@angular/core';
import { ResourceStore } from './resource.store';
@Component({
selector: 'app-resource-list',
templateUrl: './resource-list.component.html',
})
export class ResourceListComponent implements OnInit {
newName = '';
constructor(public store: ResourceStore) {}
ngOnInit() {
// Cargar datos iniciales desde ResourceService
this.store.loadResources();
}
add() {
if (this.newName.trim()) {
const newResource = { id: Date.now(), name: this.newName };
this.store.addResource(newResource);
this.newName = '';
}
}
remove(id: number) {
this.store.removeResource(id);
}
}
Template separado: resource-list.component.html
<h2>Recursos ({{ store.resourceCount()() }})</h2>
<ul>
<li *ngFor="let resource of store.resources()">
{{ resource.name }}
<button (click)="remove(resource.id)">Eliminar</button>
</li>
</ul>
<input [(ngModel)]="newName" placeholder="Nombre del recurso" />
<button (click)="add()">Añadir Recurso</button>
Notas importantes:
-
store.resources()ystore.resourceCount()se llaman como funciones porque son Signals / computed. - La carga de datos se realiza mediante
ResourceService.get(), manteniendo la separación entre obtención de datos y gestión del estado. - Toda la lógica de estado (añadir, eliminar, actualizar) permanece centralizada en el store, dejando los componentes limpios y enfocados en la presentación.
- La reactividad es automática: al actualizar
resourcesSignal, el template se refresca sin necesidad desubscribe().
Selectores Computados
Los computed permiten derivar información del estado principal de manera reactiva:
// Filtrar recursos cuyo nombre contiene "Angular"
readonly angularResources = computed(() =>
this.resourcesSignal().filter(r => r.name.includes('Angular'))
);
// Contar recursos con un nombre específico
countResourcesByName(name: string) {
return computed(() => this.resourcesSignal().filter(r => r.name === name).length);
}
Uso en el template:
<div>Total Angular Resources: {{ store.countResourcesByName('Angular')() }}</div>
Efectos y reactividad adicional
Si se desea reaccionar automáticamente a cambios del estado:
import { effect } from '@angular/core';
effect(() => {
console.log('Número de recursos:', this.store.resourceCount());
});
Cada vez que cambie resourceCount, se ejecutará la función del effect. Es útil para sincronizaciones externas, registros de auditoría o cálculos derivados.
Buenas Prácticas
- Mantener el estado inmutable: actualizar arrays y objetos creando nuevas instancias.
- Usar
computedpara todos los valores derivados. - Separar la lógica de estado de los componentes: el store maneja la manipulación de datos y los componentes solo consumen y disparan acciones.
- Usar efectos (
effect()) únicamente para side effects, nunca para mutar el estado principal.