# Angular 18: Signals y Nuevas Funcionalidades > Angular 18 estabiliza Signals como primitiva reactiva, introduce APIs basadas en signals y habilita la deteccion de cambios sin Zone.js para aplicaciones mas performantes. - Published: 2026-01-18 - Updated: 2026-04-10 - Author: SharpSkill - Tags: angular 18, angular signals, zoneless, signal inputs, reactivity - Reading time: 12 min --- Angular 18 marca un punto de inflexion en la evolucion del framework con la estabilizacion de Signals. Esta nueva primitiva reactiva transforma fundamentalmente la forma de construir componentes Angular, ofreciendo una alternativa moderna a los decoradores tradicionales y abriendo camino hacia la deteccion de cambios sin Zone.js. > **Lo que se aprende en este articulo** > > Las APIs basadas en signals de Angular 18: input(), model(), viewChild() y la configuracion zoneless para aplicaciones mas ligeras y performantes. ## Entendiendo Signals en Angular 18 Signals representan un nuevo enfoque de reactividad en Angular. A diferencia de los decoradores clasicos como `@Input()` que dependen de la deteccion de cambios de Zone.js, Signals ofrece reactividad granular y explicita. Cada Signal encapsula un valor y notifica automaticamente a los consumidores cuando ese valor cambia. Este enfoque aporta varias ventajas: mejor rendimiento gracias a actualizaciones precisas, integracion nativa con las funciones `computed()` y `effect()`, y preparacion para el futuro zoneless de Angular. ```typescript // signals-basics.component.ts // Demonstration of fundamental Signal concepts import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Counter: {{ count() }}

Double: {{ doubleCount() }}

` }) export class CounterComponent { // Writable signal - value can be modified count = signal(0); // Computed signal - automatically derived from count // Only recalculates when count changes doubleCount = computed(() => this.count() * 2); constructor() { // Effect - executed on every count change // Useful for side effects (logs, API calls, etc.) effect(() => { console.log(`New counter value: ${this.count()}`); }); } increment() { // update() allows modification based on previous value this.count.update(value => value + 1); } decrement() { this.count.update(value => value - 1); } reset() { // set() directly replaces the value this.count.set(0); } } ``` Signals funcionan como contenedores reactivos: `signal()` crea un Signal de escritura, `computed()` deriva valores calculados y `effect()` permite ejecutar acciones en respuesta a cambios. ## Signal Inputs con input() La funcion `input()` reemplaza al decorador tradicional `@Input()`. Retorna un `InputSignal` de solo lectura, garantizando que los datos siempre fluyan de padre a hijo sin modificaciones accidentales. ```typescript // book-card.component.ts // Component using signal inputs import { Component, input, computed } from '@angular/core'; interface Book { id: string; title: string; author: string; price: number; discountPercent?: number; } @Component({ selector: 'app-book-card', standalone: true, template: `

{{ book().title }}

By {{ book().author }}

@if (hasDiscount()) {

\${{ book().price }} \${{ discountedPrice() }}

} @else {

\${{ book().price }}

} @if (featured()) { Featured }
` }) export class BookCardComponent { // Required input - template won't compile without this prop book = input.required(); // Optional input with default value featured = input(false); // Computed based on input - automatically recalculated hasDiscount = computed(() => { const discount = this.book().discountPercent; return discount !== undefined && discount > 0; }); // Discounted price calculation discountedPrice = computed(() => { const { price, discountPercent } = this.book(); if (!discountPercent) return price; return (price * (100 - discountPercent) / 100).toFixed(2); }); } ``` El uso en un template padre se mantiene similar, pero con type safety y reactividad Signal: ```typescript // book-list.component.ts // Parent component using book-card import { Component, signal } from '@angular/core'; import { BookCardComponent } from './book-card.component'; @Component({ selector: 'app-book-list', standalone: true, imports: [BookCardComponent], template: `
@for (book of books(); track book.id) { }
` }) export class BookListComponent { books = signal([ { id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 }, { id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 } ]); featuredBookId = signal('1'); } ``` La diferencia principal con `@Input()`: los signal inputs son de solo lectura. Llamar a `this.book.set()` desde el componente hijo es imposible, lo que refuerza el flujo de datos unidireccional. ## Two-Way Binding con model() Para los casos que requieren sincronizacion bidireccional, Angular 18 introduce `model()`. Esta funcion crea un Signal de escritura que propaga automaticamente los cambios al componente padre. ```typescript // search-input.component.ts // Component with bidirectional binding via model() import { Component, model, output, computed } from '@angular/core'; @Component({ selector: 'app-search-input', standalone: true, template: `
@if (query().length > 0) { } {{ charCount() }} characters
` }) export class SearchInputComponent { // model() creates a bidirectional Signal // Modifications propagate to parent query = model(''); // Classic input for configuration placeholder = model('Search...'); // Output for additional events searchSubmitted = output(); // Computed based on model charCount = computed(() => this.query().length); onInput(event: Event) { const value = (event.target as HTMLInputElement).value; // Update model - propagates to parent this.query.set(value); } clear() { this.query.set(''); } submit() { if (this.query().length > 0) { this.searchSubmitted.emit(this.query()); } } } ``` El componente padre utiliza la sintaxis banana-in-a-box `[()]` para el binding bidireccional: ```typescript // app.component.ts // Using two-way binding with model() import { Component, signal, effect } from '@angular/core'; import { SearchInputComponent } from './search-input.component'; @Component({ selector: 'app-root', standalone: true, imports: [SearchInputComponent], template: `

Current search: {{ searchTerm() }}

@for (result of filteredResults(); track result.id) {
{{ result.name }}
}
` }) export class AppComponent { // Local signal synchronized with child component searchTerm = signal(''); results = signal([ { id: 1, name: 'Angular 18' }, { id: 2, name: 'React 19' }, { id: 3, name: 'Vue 3' } ]); // Reactive filtering based on searchTerm filteredResults = computed(() => { const term = this.searchTerm().toLowerCase(); if (!term) return this.results(); return this.results().filter(r => r.name.toLowerCase().includes(term) ); }); } ``` > **model() vs input()** > > Usar `input()` para datos de solo lectura (padre a hijo). Usar `model()` cuando el componente hijo necesita modificar el valor (bidireccional). ## Signal Queries con viewChild() y contentChild() Las funciones `viewChild()`, `viewChildren()`, `contentChild()` y `contentChildren()` reemplazan a sus decoradores correspondientes. Retornan Signals, eliminando la necesidad de lifecycle hooks como `ngAfterViewInit`. ```typescript // form-container.component.ts // Demonstration of signal queries import { Component, viewChild, viewChildren, ElementRef, effect, signal } from '@angular/core'; import { FormFieldComponent } from './form-field.component'; @Component({ selector: 'app-form-container', standalone: true, imports: [FormFieldComponent], template: `
` }) export class FormContainerComponent { // viewChild returns Signal formElement = viewChild('formElement'); // viewChild.required guarantees element exists firstInput = viewChild.required>('firstInput'); // Query on a component - returns the component itself firstFormField = viewChild(FormFieldComponent); // viewChildren for multiple elements allFormFields = viewChildren(FormFieldComponent); constructor() { // Effect replaces ngAfterViewInit for queries effect(() => { // Signal is automatically resolved const input = this.firstInput(); console.log('First input available:', input.nativeElement); }); // React to list changes effect(() => { const fields = this.allFormFields(); console.log(`${fields.length} form fields found`); }); } focusFirst() { // Direct access via Signal this.firstInput().nativeElement.focus(); } onSubmit(event: Event) { event.preventDefault(); // Access the form const form = this.formElement(); if (form) { console.log('Form submitted'); } } } ``` Para proyectar contenido y acceder a el, `contentChild()` funciona de manera similar: ```typescript // card.component.ts // Using contentChild for projected content import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, template: `
@if (hasFooter()) {
}
` }) export class CardComponent { // Detect if footer was projected footerContent = contentChild('[card-footer]'); // Computed to check footer presence hasFooter = computed(() => this.footerContent() !== undefined); } ``` ## Deteccion de Cambios Zoneless Angular 18 introduce la deteccion de cambios sin Zone.js en modo experimental. Esta funcionalidad reduce el tamano del bundle en aproximadamente 13 KB y mejora el rendimiento al eliminar los monkey-patches en las APIs asincronas del navegador. ```typescript // main.ts // Configuring the application in zoneless mode import { bootstrapApplication } from '@angular/platform-browser'; import { provideExperimentalZonelessChangeDetection } from '@angular/core'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ // Enable experimental zoneless detection provideExperimentalZonelessChangeDetection() ] }); ``` La configuracion de `angular.json` tambien debe actualizarse para remover Zone.js: ```json { "projects": { "my-app": { "architect": { "build": { "options": { "polyfills": [] } } } } } } ``` En modo zoneless, la deteccion de cambios se activa automaticamente en estos casos: actualizacion de Signal, llamada a `markForCheck()`, nuevo valor recibido via AsyncPipe, o attach/detach de componente. ```typescript // zoneless-counter.component.ts // Component optimized for zoneless mode import { Component, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-zoneless-counter', standalone: true, // OnPush recommended for zoneless changeDetection: ChangeDetectionStrategy.OnPush, template: `

Counter: {{ count() }}

@if (loading()) {

Loading...

} @if (data()) {
{{ data() | json }}
}
` }) export class ZonelessCounterComponent { private http = inject(HttpClient); count = signal(0); loading = signal(false); data = signal(null); increment() { // Signal update triggers detection this.count.update(c => c + 1); } async fetchData() { this.loading.set(true); try { // Signals guarantee view updates const response = await fetch('/api/data'); const json = await response.json(); this.data.set(json); } finally { this.loading.set(false); } } } ``` > **Compatibilidad Zoneless** > > Los componentes que usan `ChangeDetectionStrategy.OnPush` y Signals son generalmente compatibles con el modo zoneless. Evitar modificaciones directas de propiedades que no sean Signals. ## Migracion de Componentes Existentes La migracion a APIs basadas en signals puede realizarse de forma gradual. A continuacion, un ejemplo de refactorizacion de un componente tradicional: ```typescript // BEFORE: Component with classic decorators // user-profile-legacy.component.ts import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-user-profile-legacy', template: `

{{ user.name }}

{{ user.email }}

` }) export class UserProfileLegacyComponent implements AfterViewInit { @Input() user!: { name: string; email: string }; @ViewChild('container') container!: ElementRef; ngAfterViewInit() { console.log('Container ready:', this.container.nativeElement); } } ``` ```typescript // AFTER: Component migrated to Signals // user-profile.component.ts import { Component, input, viewChild, ElementRef, effect } from '@angular/core'; interface User { name: string; email: string; } @Component({ selector: 'app-user-profile', standalone: true, template: `

{{ user().name }}

{{ user().email }}

` }) export class UserProfileComponent { // input.required replaces @Input() with ! user = input.required(); // viewChild.required replaces @ViewChild with ! container = viewChild.required('container'); constructor() { // effect replaces ngAfterViewInit for queries effect(() => { console.log('Container ready:', this.container().nativeElement); }); } } ``` Beneficios de esta migracion: tipado mas estricto, reactividad automatica, menos codigo boilerplate y compatibilidad con el modo zoneless. ## Buenas Practicas con Signals Recomendaciones clave para aprovechar al maximo Signals en Angular 18: ```typescript // best-practices.component.ts // Example of best practices with Signals import { Component, signal, computed, effect, untracked, ChangeDetectionStrategy } from '@angular/core'; interface Product { id: string; name: string; price: number; quantity: number; } @Component({ selector: 'app-cart', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

Cart ({{ itemCount() }} items)

@for (item of items(); track item.id) {
{{ item.name }} {{ item.quantity }} × \${{ item.price }}
}
Total: \${{ total() }}
` }) export class CartComponent { // Signal for mutable data items = signal([]); // Computed for derived values - avoids unnecessary recalculations itemCount = computed(() => this.items().length); total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.quantity, 0) ); constructor() { // Effect for side effects (analytics, persistence) effect(() => { const currentItems = this.items(); // untracked avoids creating a dependency untracked(() => { localStorage.setItem('cart', JSON.stringify(currentItems)); }); }); } addItem(product: Product) { // update() for modifications based on previous state this.items.update(current => { const existing = current.find(i => i.id === product.id); if (existing) { return current.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i ); } return [...current, { ...product, quantity: 1 }]; }); } removeItem(id: string) { this.items.update(current => current.filter(i => i.id !== id)); } } ``` Puntos clave a recordar: - Usar `computed()` para valores derivados en lugar de recalcular en el template - Preferir `update()` sobre `set()` cuando el nuevo valor depende del anterior - Usar `untracked()` en effects para evitar dependencias circulares - Siempre especificar `track` en los bucles `@for` para optimizar el renderizado ## Conclusion Angular 18 sienta las bases para un futuro sin Zone.js a traves de Signals. Puntos clave: - **input()** reemplaza a `@Input()` con tipado mas estricto y acceso de solo lectura garantizado - **model()** habilita two-way binding reactivo entre padre e hijo - **viewChild()** y **contentChild()** eliminan la necesidad de lifecycle hooks - **Zoneless** reduce el tamano del bundle y mejora el rendimiento - **computed()** y **effect()** completan el ecosistema reactivo - La migracion gradual es posible componente por componente Adoptar Signals prepara las aplicaciones Angular para futuras versiones donde el modo zoneless sera la norma. Esta transicion representa una inversion inteligente para la mantenibilidad y el rendimiento a largo plazo. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/angular/angular-18-signals-new-features