Capítulo 36 - Parte 1: @let en templates y signal-based queries¶
Parte 1 de 4 · Capítulo 36 · PARTE XV - Angular 20 y el Futuro del Framework
Angular 20, lanzado en mayo de 2025, consolidó dos características que transforman la forma de escribir templates y consultar el DOM: la declaración de variables con @let y las signal-based queries que reemplazan a @ViewChild y @ContentChild con la ergonomía de los Signals.
Declaración de variables con @let¶
Antes de Angular 20, el único modo de crear variables locales en un template era el poco intuitivo *ngLet de terceros o el abuso de async pipe con as. La nueva sintaxis @let es limpia, declarativa y funciona dentro de cualquier bloque de control de flujo:
<!-- Template de un componente de perfil -->
@let usuario = authService.usuario();
@let nombreCompleto = usuario.nombre + ' ' + usuario.apellido;
@let esAdmin = usuario.roles.includes('admin');
<header>
<h1>Bienvenido, {{ nombreCompleto }}</h1>
@if (esAdmin) {
<span class="badge-admin">Administrador</span>
}
</header>
@let pedidosRecientes = pedidosService.pedidos() | slice:0:5;
@for (pedido of pedidosRecientes; track pedido.id) {
<app-tarjeta-pedido [pedido]="pedido" />
}
Las variables declaradas con @let son:
- Reactivas: si el valor de la derecha es un Signal o un pipe
async, se recalculan automáticamente en cada detección de cambios. - Locales al bloque: una variable declarada dentro de
@ifno es visible fuera de él. - De solo lectura: no se pueden reasignar desde el template ni desde la clase.
<!-- @let dentro de bloques - el scope es el bloque que la contiene -->
@if (producto(); as p) {
@let descuento = p.precio * 0.1;
<p>Precio con descuento: {{ p.precio - descuento | currency }}</p>
}
<!-- "descuento" no es accesible aquí -->
El caso de uso más valioso es evitar llamadas repetidas a métodos o Signals costosos dentro del template:
<!-- Sin @let: httpResource().value() se llama tres veces -->
<p>{{ productosResource.value()?.total }}</p>
<ul>@for (p of productosResource.value()?.items; track p.id) { ... }</ul>
<!-- Con @let: se llama una vez, el resultado se reutiliza -->
@let datos = productosResource.value();
<p>{{ datos?.total }}</p>
<ul>@for (p of datos?.items ?? []; track p.id) { ... }</ul>
Signal-based queries: viewChild, viewChildren, contentChild, contentChildren¶
Las queries basadas en decoradores (@ViewChild, @ContentChild) tienen un problema: el resultado no está disponible hasta ngAfterViewInit, y actualizarlo requiere ChangeDetectorRef.markForCheck() manualmente con OnPush. Las signal-based queries resuelven ambos problemas.
import { Component, viewChild, viewChildren, ElementRef, QueryList } from '@angular/core';
@Component({
selector: 'app-formulario',
standalone: true,
template: `
<input #campoNombre type="text" />
<input #campoClave type="password" />
<button #botonEnviar>Enviar</button>
`,
})
export class FormularioComponent {
// Signal que contiene la referencia (undefined si no existe en el DOM)
campoNombre = viewChild<ElementRef<HTMLInputElement>>('campoNombre');
botonEnviar = viewChild<ElementRef<HTMLButtonElement>>('botonEnviar');
// Signal que contiene un array con todas las referencias
todosLosCampos = viewChildren<ElementRef<HTMLInputElement>>('campoNombre, campoClave');
enfocarPrimerCampo(): void {
// No necesita ngAfterViewInit - el Signal ya tiene el valor
this.campoNombre()?.nativeElement.focus();
}
deshabilitarBoton(): void {
const boton = this.botonEnviar();
if (boton) boton.nativeElement.disabled = true;
}
}
La diferencia clave respecto a @ViewChild es que el resultado es un Signal tipado: Signal<ElementRef | undefined>. Esto permite usarlo en computed() y effect() de forma reactiva:
import { Component, viewChild, ElementRef, effect } from '@angular/core';
@Component({ selector: 'app-canvas', standalone: true, template: `<canvas #lienzo></canvas>` })
export class CanvasComponent {
lienzo = viewChild<ElementRef<HTMLCanvasElement>>('lienzo');
constructor() {
// effect() reacciona automáticamente cuando el canvas aparece o desaparece
effect(() => {
const canvas = this.lienzo();
if (canvas) {
// El canvas acaba de aparecer en el DOM
this.inicializarWebGL(canvas.nativeElement);
}
});
}
private inicializarWebGL(canvas: HTMLCanvasElement): void {
// ...
}
}
contentChild y contentChildren para proyección de contenido¶
Las queries de contenido proyectado también tienen su versión con Signals:
import { Component, contentChild, contentChildren, ElementRef } from '@angular/core';
import { BotonesComponent } from './botones.component';
@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal-body"><ng-content /></div>
<div class="modal-footer"><ng-content select="[acciones]" /></div>
`,
})
export class ModalComponent {
// Busca un único elemento hijo por tipo de componente
botones = contentChild(BotonesComponent);
// Busca todos los elementos con un selector de atributo
accionesExtra = contentChildren<ElementRef>('[accion-extra]');
}
Comparativa: decoradores vs signal queries¶
flowchart TD
subgraph Decoradores["@ViewChild / @ContentChild"]
DEC[Resultado disponible\nen ngAfterViewInit] --> CDREF[Requiere markForCheck\ncon OnPush]
CDREF --> NOREAC[No reactivo:\nnecesita lógica manual]
end
subgraph SignalQ["viewChild() / contentChild()"]
SIG[Disponible inmediatamente\ncomo Signal] --> REACT[Reactivo en computed\ny effect automaticamente]
REACT --> NOCK[No necesita markForCheck]
end
| Característica | @ViewChild |
viewChild() |
|---|---|---|
| Disponibilidad | Desde ngAfterViewInit |
Inmediata (Signal) |
Reactivo en computed |
No | Sí |
Compatible con effect |
No | Sí |
| Tipo del resultado | T \| undefined |
Signal<T \| undefined> |
Requiere markForCheck con OnPush |
Sí | No |
La recomendación para código nuevo en Angular 20+ es usar siempre viewChild() y contentChild(). Los decoradores siguen funcionando pero quedan como API legada.
Puntos clave¶
@let variable = expresiondeclara variables locales reactivas en templates, eliminando repetición y mejorando la legibilidad- Las variables
@lettienen scope del bloque en que se declaran y son de solo lectura viewChild()yviewChildren()retornan Signals, disponibles inmediatamente sin esperarngAfterViewInit- Las signal queries son reactivas en
computed()yeffect()sin necesidad demarkForCheck() contentChild()ycontentChildren()son las versiones para contenido proyectado conng-content
¿Qué sigue?¶
En la Parte 2 cubrimos linkedSignal() estable y las APIs resource() y httpResource() que pasaron a developer preview en Angular 20.