En esta página
Effects, linkedSignal y resource
Effects: efectos secundarios reactivos
Un effect() ejecuta una función cada vez que los signals que lee cambian. Es la herramienta para efectos secundarios: sincronizar con localStorage, enviar analytics, hacer logging, etc.
import { signal, effect } from '@angular/core';
const tema = signal<'claro' | 'oscuro'>('oscuro');
// Se ejecuta al crear y cada vez que tema() cambia
effect(() => {
document.documentElement.setAttribute('data-theme', tema());
});Reglas de los effects
- Solo en contexto de inyección — Dentro de un constructor, un campo del componente o
runInInjectionContext - Se limpian automaticamente — Cuando el componente se destruye
- No deben escribir en signals — Salvo con
allowSignalWrites(no recomendado)
Cleanup en effects
Si tu effect registra listeners o timers, puedes limpiarlos con onCleanup:
effect((onCleanup) => {
const id = setInterval(() => {
console.log('Tick para:', usuarioId());
}, 1000);
onCleanup(() => clearInterval(id));
});linkedSignal: signal derivado escribible
linkedSignal crea un signal que se computa automaticamente cuando su fuente cambia, pero que también puedes escribir manualmente:
import { signal, linkedSignal } from '@angular/core';
const categoriaId = signal(1);
// Se resetea a 1 cada vez que categoriaId cambia
const página = linkedSignal<number>({
source: categoriaId,
computation: () => 1,
});
// Puedes escribirlo manualmente
página.set(3);
// Pero al cambiar categoriaId, vuelve a 1
categoriaId.set(2); // página() === 1Casos de uso tipicos:
- Paginación que se resetea al cambiar filtros
- Pestana activa que vuelve a la primera al navegar
- Formularios que se reinician al seleccionar otro registro
resource: carga asincrona reactiva
La API resource conecta signals con operaciones asincronas. Cuando las dependencias cambian, el resource se recarga automaticamente:
import { signal, resource } from '@angular/core';
const busqueda = signal('angular');
const resultados = resource({
request: () => ({ q: busqueda() }),
loader: async ({ request, abortSignal }) => {
const resp = await fetch(
`https://api.ejemplo.com/buscar?q=${request.q}`,
{ signal: abortSignal },
);
return resp.json();
},
});Propiedades del resource
| Propiedad | Tipo | Descripcion |
|---|---|---|
.value() |
T | undefined |
El valor cargado |
.isLoading() |
boolean |
Si esta cargando |
.error() |
unknown |
El error, si ocurrio |
.status() |
ResourceStatus |
Estado detallado |
.reload() |
void |
Recarga manualmente |
rxResource para Observables
Si prefieres trabajar con Observables (ej. HttpClient), usa rxResource:
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
const http = inject(HttpClient);
const id = signal(1);
const datos = rxResource({
request: () => ({ id: id() }),
loader: ({ request }) => http.get(`/api/datos/${request.id}`),
});Práctica
- Crea un effect de persistencia: Implementa un signal para el tema (
'claro' | 'oscuro') y uneffect()que guarde el valor enlocalStoragecada vez que cambie. - Usa linkedSignal: Crea un signal
categoriaIdy unlinkedSignalpara la pagina actual que se resetee a 1 cada vez que cambie la categoria. Agrega botones para cambiar categoria y avanzar pagina. - Implementa un resource: Usa la API
resourcepara cargar datos dehttps://jsonplaceholder.typicode.com/users/{id}reactivamente. Muestra estados de carga, error y datos con@if.
En la siguiente leccion veremos como los componentes se comunican entre si usando inputs, outputs y model.
import {
Component, signal, computed, effect,
linkedSignal, resource,
ChangeDetectionStrategy,
} from '@angular/core';
interface Usuario {
id: number;
nombre: string;
email: string;
}
@Component({
selector: 'app-perfil',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './perfil.html',
})
export class Perfil {
readonly usuarioId = signal(1);
// linkedSignal: signal derivado que se puede escribir
readonly pestanaActiva = linkedSignal<string>({
source: this.usuarioId,
computation: () => 'info', // resetea al cambiar de usuario
});
// resource: carga asincrona reactiva
readonly usuarioResource = resource<Usuario, { id: number }>({
request: () => ({ id: this.usuarioId() }),
loader: async ({ request, abortSignal }) => {
const resp = await fetch(
`https://api.ejemplo.com/usuarios/${request.id}`,
{ signal: abortSignal },
);
if (!resp.ok) throw new Error('Error al cargar usuario');
return resp.json();
},
});
// Acceso al estado del resource
readonly usuario = computed(() => this.usuarioResource.value());
readonly cargando = computed(() => this.usuarioResource.isLoading());
readonly error = computed(() => this.usuarioResource.error());
constructor() {
// Effect para sincronizar con almacenamiento local
effect(() => {
const id = this.usuarioId();
localStorage.setItem('ultimoUsuario', String(id));
console.log('Usuario seleccionado:', id);
});
}
cambiarUsuario(id: number): void {
this.usuarioId.set(id);
}
cambiarPestana(tab: string): void {
this.pestanaActiva.set(tab);
}
}
Inicia sesión para guardar tu progreso