|
|
|
# **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
|
|
|
|
|
|
|
|
1. **Signal:** Contenedor de un valor reactivo que puede actualizarse mediante `.set()` o `.update()`.
|
|
|
|
2. **Computed:** Función derivada que calcula valores basados en Signals, actualizándose automáticamente cuando alguno de los Signals que depende cambian.
|
|
|
|
3. **Reactividad automática:** Los componentes que usan Signals se actualizan sin suscripciones explícitas.
|
|
|
|
4. **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
|
|
|
|
|
|
|
|
```ts
|
|
|
|
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
|
|
|
|
|
|
|
|
```ts
|
|
|
|
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`
|
|
|
|
|
|
|
|
```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:**
|
|
|
|
|
|
|
|
1. `store.resources()` y `store.resourceCount()` se llaman como funciones porque son Signals / computed.
|
|
|
|
2. La carga de datos se realiza mediante `ResourceService.get()`, manteniendo la separación entre obtención de datos y gestión del estado.
|
|
|
|
3. 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.
|
|
|
|
4. La reactividad es automática: al actualizar `resourcesSignal`, el template se refresca sin necesidad de `subscribe()`.
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
## Selectores Computados
|
|
|
|
|
|
|
|
Los _computed_ permiten derivar información del estado principal de manera reactiva:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
// 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:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<div>Total Angular Resources: {{ store.countResourcesByName('Angular')() }}</div>
|
|
|
|
```
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
## Efectos y reactividad adicional
|
|
|
|
|
|
|
|
Si se desea reaccionar automáticamente a cambios del estado:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
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
|
|
|
|
|
|
|
|
1. Mantener el estado inmutable: actualizar arrays y objetos creando nuevas instancias.
|
|
|
|
2. Usar `computed` para todos los valores derivados.
|
|
|
|
3. 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.
|
|
|
|
4. Usar efectos (`effect()`) únicamente para side effects, nunca para mutar el estado principal.
|
|
|
|
|
|
|
|
--- |