← maurobernal.com.ar

Angular Signals en el Mundo Real: Inputs Reactivos y Routing Inteligente

Angular Signals llegó para transformar la forma en que construimos aplicaciones reactivas. En este artículo exploramos escenarios del mundo real donde los Signals cambian radicalmente el diseño de tus componentes Angular: desde inputs reactivos hasta routing con parámetros de ruta y llamadas a APIs declarativas con httpResource().

El problema con ngOnChanges: el viejo paradigma imperativo

Durante años, reaccionar a cambios en los inputs de un componente implicaba escribir código verboso y frágil usando ngOnChanges. Por cada nuevo @Input(), la complejidad crecía de forma no lineal, con lógica condicional duplicada y errores difíciles de detectar.

// Enfoque tradicional con ngOnChanges
@Component({ selector: 'legacy-config-panel', template: '...' })
export class LegacyConfigPanel implements OnChanges {
  @Input() userRole!: string;
  @Input() theme!: 'light' | 'dark';

  isAdminView = false;
  isDarkMode = false;

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userRole']) this.isAdminView = this.userRole === 'admin';
    if (changes['theme']) this.isDarkMode = this.theme === 'dark';
  }
}

Angular Signals: diseño declarativo con input() y computed()

Con Angular Signals, cada pieza de estado derivado se expresa como una función de sus inputs. No más ngOnChanges, no más boilerplate. Angular garantiza que cuando un input signal cambia, los valores computed() se actualizan automáticamente.

@Component({ selector: 'config-panel', template: '...' })
export class ConfigPanel {
  userRole = input<string>();
  theme = input<'light' | 'dark'>();
  region = input<string>();
  featureFlags = input<string[]>();

  readonly isAdminView = computed(() => this.userRole() === 'admin');
  readonly isDarkMode = computed(() => this.theme() === 'dark');
  readonly showLegalBanner = computed(() =>
    this.region() === 'EU' &&
    this.userRole() !== 'admin' &&
    !this.featureFlags()?.includes('suppress-legal')
  );
}

Este enfoque establece un nuevo contrato entre componentes: los inputs son valores vivos, el estado derivado es explícito y las actualizaciones son automáticas.

Queries Reactivas: viewChild() y contentChild()

Los decoradores @ViewChild y @ContentChild tradicionales eran lookups estáticos atados al ciclo de vida del componente. Con Signal-based queries, obtenemos una Signal<T | undefined> verdaderamente reactiva.

// Antes: dependiente del ciclo de vida
@ViewChild(CustomCardHeader) header!: CustomCardHeader;
ngAfterViewInit() {
  if (this.header) this.header.setFocus();
}

// Ahora: completamente reactivo
header = viewChild(CustomCardHeader);
readonly hasHeader = computed(() => !!this.header());

effect(() => {
  if (this.header()) this.header()!.setFocus();
});

El código dentro de effect() se re-ejecuta automáticamente cuando el componente entra o sale del DOM, sin necesidad de gestionar ngAfterViewInit ni ExpressionChangedAfterItHasBeenChecked.

Two-Way Binding Moderno: la API model()

La nueva API model() simplifica radicalmente el two-way binding. En lugar de coordinar manualmente un @Input() con su correspondiente @Output(), basta con declarar un WritableSignal con model().

// Antes
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
increment() { this.valueChange.emit(this.value + 10); }

// Ahora con model()
export class CustomSlider {
  value = model(0);
  increment() { this.value.update(old => old + 10); }
}

// En el componente padre
<custom-slider [(value)]="volume" />

Nota importante: model() no reemplaza ControlValueAccessor todavía. CVA sigue siendo necesario para integración con FormControl y formControlName. Sin embargo, muchos casos de uso que antes requerían CVA ahora pueden resolverse con model().

Parámetros de Ruta como Signals con withComponentInputBinding()

Con withComponentInputBinding(), los parámetros de ruta se mapean directamente a inputs del componente como signals. Adiós a ActivatedRoute, adiós a subscripciones manuales.

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
  ],
};

// project-details.component.ts
export class ProjectDetailsComponent {
  // Angular mapea ':projectId' automáticamente como Signal
  projectId = input<string>();
}

Carga de Datos Reactiva con httpResource()

La API experimental httpResource() es quizás el caso de uso más poderoso: conectar directamente un Signal de parámetro de ruta con una llamada HTTP, obteniendo tres signals listos para usar: data, loading y error.

@Component({
  standalone: true,
  template: 
    @if (loading()) { <p>Cargando…</p> }
    @if (error()) { <p>Error: {{ error() }}</p> }
    @if (data(); as project) {
      <h1>{{ project.name }}</h1>
      <p>{{ project.description }}</p>
    }
  
})
export class ProjectDetailsComponent {
  projectId = input.required<string>();

  resource = httpResource(() => /api/projects/);

  data = this.resource.data;
  loading = this.resource.loading;
  error = this.resource.error;
}

Cuando projectId() cambia (por ejemplo, al navegar entre proyectos), httpResource() lanza automáticamente una nueva petición HTTP. Todo el flujo — desde el parámetro de ruta hasta la respuesta del servidor — es reactivo y declarativo.

Conclusión: El futuro de Angular es reactivo y declarativo

Angular Signals no es solo una nueva sintaxis — es una nueva forma de pensar el diseño de componentes:

  • input() + computed(): Elimina ngOnChanges y lógica imperativa
  • viewChild() / contentChild(): Queries reactivas sin lifecycle hooks
  • model(): Two-way binding declarativo sin boilerplate
  • withComponentInputBinding(): Parámetros de ruta como signals
  • httpResource(): API calls declarativas y reactivas

La combinación de todos estos elementos permite construir aplicaciones donde los datos fluyen de forma coherente desde los parámetros de ruta hasta la UI, con mínimo código de pegamento y máxima legibilidad.

Artículo basado en Angular Signals in the Real World de CODE Magazine.