# 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: `
`
})
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: `
`
})
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