---
name: angular-avanzado
description: >
  Angular v17+ avanzado: Signals en profundidad, Angular zoneless, bloques defer,
  view transitions, SSR/SSG, HTTP client con signals, guards/resolvers/interceptors
  funcionales, inject(), effect(), resource API.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para Angular básico (standalone, @if/@for, signals simples, inputs/outputs) — para fundamentos cargar `angular-moderno`."
  - "No cargar para React, Vue o Svelte — SSR, signals y zoneless son patrones específicos de Angular."
  - "No cargar para errores de compilación de TypeScript en Angular — para errores de build cargar `typescript-diagnosticos`."
  - "No cargar para testing de componentes Angular — para testing con TestBed o harnesses usar el skill de testing del stack."
evolvable: true  # default para skill estandar
---
# Angular Avanzado — v17+ con Signals y APIs Modernas

## Cuándo NO cargar

- El trabajo es Angular básico: standalone components, `@if`/`@for`, signals simples, inputs/outputs — cargar `angular-moderno`.
- El framework es React, Vue o Svelte — SSR, zoneless y Resource API son patrones específicos de Angular.
- Los errores son de compilación TypeScript en el proyecto Angular — cargar `typescript-diagnosticos`.
- La tarea es escribir tests con TestBed, harnesses de Material o Spectator — usar el skill de testing del stack.

## Regla Central

> En Angular moderno, la pregunta no es "como hago esto con RxJS?", sino "es esto
> un estado reactivo (signal) o un flujo de eventos asíncronos (observable)?"

**Signals para:** Estado del componente, derivaciones sincrónicas (`computed()`), inputs y outputs.

**Observables para:** Streams de eventos (WebSocket, SSE), operaciones HTTP con transformaciones, efectos con cleanup.

---

## Signals — Uso Correcto y Anti-Patrones

NUNCA usar `effect()` para calcular valores derivados. Usar `computed()`:

```typescript
// BIEN — computed() para derivaciones sincrónicas
export class CarritoComponent {
  readonly items = signal<Item[]>([]);
  readonly total      = computed(() => this.items().reduce((s, i) => s + i.precio * i.cantidad, 0));
  readonly itemCount  = computed(() => this.items().reduce((s, i) => s + i.cantidad, 0));
  readonly tieneItems = computed(() => this.items().length > 0);
}
```

---

## effect() — Cuándo Usarlo y Cuándo No

SOLO usar `effect()` para sincronizar con el DOM o APIs externas:

```typescript
// BIEN — sincronizar clase CSS con el tema
effect(() => {
  const tema = this.temaService.temaActual();
  this.document.body.classList.toggle("tema-oscuro", tema === "oscuro");
});

// BIEN — cleanup para subscripciones
effect(onCleanup => {
  const sub = this.socket.mensajes$.subscribe(msg => { /* procesar */ });
  onCleanup(() => sub.unsubscribe());
});
```

---

## Input/Output Signals — API Moderna (v17.1+)

```typescript
@Component({ standalone: true, template: `...` })
export class TarjetaProductoComponent {
  readonly producto     = input.required<Producto>();
  readonly modoCompacto = input(false);
  readonly agregado     = output<Producto>();

  readonly precioFinal = computed(() =>
    this.producto().precio * (1 - this.precioConDescuento() / 100)
  );
}
```

---

## inject() — Inyección sin Constructor

```typescript
// Funciones reutilizables con inject()
function usarNotificaciones() {
  const snackBar = inject(MatSnackBar);
  return {
    exito: (mensaje: string) =>
      snackBar.open(mensaje, "OK", { duration: 3000, panelClass: "snack-exito" }),
    error: (mensaje: string) =>
      snackBar.open(mensaje, "Cerrar", { duration: 5000, panelClass: "snack-error" }),
  };
}

// Uso en componente — sin constructor inflado
@Component({ standalone: true, ... })
export class PerfilComponent {
  private perfilService = inject(PerfilService);
  private notificaciones = usarNotificaciones();
}
```

---

## Bloques @defer — Carga Diferida Inteligente

```html
<!-- Cargar componente pesado sólo cuando sea visible -->
@defer (on viewport) {
  <app-grafico-estadisticas [datos]="datos()" />
} @placeholder {
  <div class="h-64 bg-gray-100 rounded animate-pulse"></div>
} @loading (minimum 500ms) {
  <mat-spinner diameter="40" />
} @error {
  <p class="text-red-600">No se pudo cargar el gráfico</p>
}

<!-- Cargar al hacer hover en el botón -->
@defer (on hover(botonImportar)) {
  <app-modal-importar (cerrar)="mostrarImportar.set(false)" />
}
```

---

## Ejemplos Avanzados

Para implementaciones completas de Resource API (v19+), Guards/Resolvers/Interceptors
funcionales, SSR/SSG con Angular Universal, TransferState, y Angular Zoneless,
ver [recursos/ejemplos-avanzados.md](recursos/ejemplos-avanzados.md).

---

## Gotchas / Errores comunes no obvios

**`effect()` que modifica una signal dentro de su propio cuerpo lanza `NG0600` en runtime**: si un `effect()` escribe en una signal (con `.set()` o `.update()`) que también lee, Angular detecta un ciclo y lanza error. Causa: los efectos en Angular no están diseñados para ser fuente de cambio de signals — ese rol es de `computed()` o de eventos de usuario. Fix: mover la lógica de transformación a `computed()` o usar `effect(() => { /* ... */ }, { allowSignalWrites: true })` solo en casos documentados donde es inevitable.

**SSR con Angular Universal: acceder a `window`, `document` o `localStorage` en el constructor causa `ReferenceError`**: en el servidor no existen las APIs del browser — el código que corre en el constructor o en `ngOnInit` sin guardia puede fallar silenciosamente o lanzar en el servidor. Causa: Angular Universal renderiza el componente en Node.js antes de hidratarlo en el cliente. Fix: envolver con `isPlatformBrowser(this.platformId)` o usar `afterNextRender(() => { /* código de browser */ })` que solo se ejecuta en el cliente.

**`resource()` (API v19+) no cancela la petición anterior si el valor de la signal cambia antes de que la primera termine**: si el usuario cambia el filtro rápido, `resource()` puede retornar los datos de la segunda petición antes que la primera y luego sobre-escribirlos con los datos de la primera (más lenta). Causa: `resource()` no implementa cancelación de AbortController por defecto en versiones iniciales. Fix: en cargas sensibles al orden, usar `rxResource()` con `switchMap` que garantiza cancelación, o implementar manualmente AbortController dentro del loader de `resource()`.

**`TransferState` no transfiere valores si el servidor y el cliente usan claves distintas**: el servidor serializa con `makeStateKey<T>('clave')` y el cliente debe usar exactamente la misma key — si el componente tiene lógica condicional que genera keys dinámicas, puede que el cliente no encuentre el estado transferido y haga la petición duplicada. Causa: `TransferState` es un mapa key-value serializado en el HTML; la key debe ser determinista. Fix: extraer las StateKeys como constantes compartidas entre el path de servidor y de cliente.

## Checklist de Revisión — Angular Avanzado

- [ ] `computed()` para derivaciones, NUNCA `effect()` para calcular valores.
- [ ] `input()` y `output()` funcionales en lugar de `@Input`/`@Output` decoradores.
- [ ] `inject()` en lugar de constructor inflado con parámetros.
- [ ] `@if`/`@for`/`@defer` en lugar de `*ngIf`/`*ngFor`/`ng-template`.
- [ ] Guards, resolvers e interceptors son funciones, no clases con `implements`.
- [ ] `resource()` para carga de datos simple; RxJS para streams de eventos.
- [ ] SSR protegido con `isPlatformBrowser` antes de acceder a APIs del browser.
- [ ] `TransferState` para evitar doble petición HTTP en hidratación SSR.
- [ ] `ChangeDetectionStrategy.OnPush` en todos los componentes.
- [ ] `takeUntilDestroyed()` en todas las subscripciones que no son de resource/async pipe.
