Angular Signals y Computed en 2026: Reactividad Fina y Preguntas de Entrevista
Dominar Signals de Angular, computed, effect y linkedSignal en Angular 20+. Patrones de reactividad granular y preparación para entrevistas técnicas.

Los Signals de Angular representan el cambio más significativo en el modelo de reactividad del framework desde su creación. Introducidos en Angular 17 y ahora el enfoque predeterminado en Angular 20+, los signals proporcionan reactividad granular que elimina ciclos de detección de cambios innecesarios y se integra perfectamente con aplicaciones zoneless.
Los signals son primitivas reactivas síncronas y sin glitches. Un signal computed recalcula solo cuando sus dependencias cambian y solo una vez por ciclo de cambio, sin importar cuántas veces se lea.
Comprendiendo las Primitivas Signal en Angular 20
El sistema de signals de Angular consta de tres primitivas principales: signal(), computed() y effect(). Cada una cumple un propósito distinto en el grafo reactivo.
Un signal escribible contiene un valor que puede actualizarse con .set() o .update(). La lectura del signal devuelve el valor actual, y cualquier signal computed o effect que dependa de él recibe notificación de los cambios.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal with initial value
count = signal(0);
// Computed signal derives from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() receives current value as argument
this.count.update(c => c + 1);
}
}La diferencia clave con BehaviorSubject de RxJS: los signals son síncronos y sin glitches. Cuando count cambia, doubleCount recalcula exactamente una vez, incluso si se lee múltiples veces en el mismo bloque síncrono.
Signals Computed: Evaluación Perezosa y Memoización
Los signals computed almacenan en caché su resultado hasta que una dependencia cambia. Esta memoización es automática, a diferencia de RxJS donde shareReplay o distinctUntilChanged deben agregarse manualmente.
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed tracks only the properties it reads
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Depends on another computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Un signal computed se evalúa de manera perezosa: la función de cálculo se ejecuta solo cuando el signal se lee por primera vez, no cuando se declara. Las lecturas subsiguientes devuelven el valor en caché hasta que una dependencia lo invalide.
Effect: Efectos Secundarios en el Grafo Reactivo
Los effects ejecutan código en respuesta a cambios de signals. Se ejecutan de manera asíncrona durante el ciclo de detección de cambios, después de que todas las actualizaciones síncronas se completan.
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect syncs signal state to DOM attribute
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect for localStorage persistence
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Los effects rastrean dependencias dinámicamente. Si una rama condicional no se toma, los signals leídos solo en esa rama no se rastrean hasta que la condición cambie.
Usar effect para copiar datos de un signal a otro indica un problema de diseño. Es preferible usar computed para estado derivado o linkedSignal cuando el valor derivado necesita ser modificable.
linkedSignal: Estado Derivado Modificable
Introducido en Angular 19 y estable en Angular 20, linkedSignal crea un signal modificable que se reinicia cuando su fuente cambia. Resuelve escenarios donde computed es de solo lectura pero el valor derivado necesita modificación local.
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal resets to 1 when pageSize or totalItems changes
// but allows manual navigation via prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}Sin linkedSignal, este patrón requería un effect escribiendo a otro signal (anti-patrón) o una orquestación RxJS compleja.
¿Listo para aprobar tus entrevistas de Angular?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
APIs de Componentes Basadas en Signals: input, model, viewChild
Angular 20 promueve las APIs de componentes basadas en signals a estado estable. Estas reemplazan decoradores con alternativas reactivas que se integran en el grafo de signals.
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input replaces @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creates two-way bindable signal
// Parent uses [(selected)]="parentSignal"
selected = model(false);
// Output emitter for events
cardClicked = output<void>();
// viewChild returns Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed derives from signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Los inputs basados en signals proporcionan mejor seguridad de tipos: input.required() devuelve InputSignal<T> mientras que input() devuelve InputSignal<T | undefined> a menos que se proporcione un valor predeterminado.
Signals vs RxJS: Rendimiento Bajo Carga
Los benchmarks en Angular 21+ demuestran que los signals reducen la sobrecarga de detección de cambios en escenarios intensivos de UI. Bajo carga pesada con actualizaciones frecuentes, los signals mostraron perfiles de memoria más limpios y menor uso de CPU comparado con bindings impulsados por RxJS.
La separación de responsabilidades es clara:
| Caso de Uso | Enfoque Recomendado |
|---|---|
| Estado local del componente | signal() |
| Valores UI derivados | computed() |
| Sincronización con APIs externas | effect() |
| Peticiones HTTP | Observable RxJS + toSignal() |
| Streams WebSocket | RxJS con operadores |
| Input de usuario con debounce | RxJS debounceTime, luego toSignal() |
| Orquestación asíncrona compleja | Pipelines RxJS |
RxJS sigue siendo la herramienta para streams asíncronos, cancelación y contrapresión. Los signals manejan estado síncrono.
Interoperabilidad RxJS: toSignal y toObservable
Angular proporciona utilidades en @angular/core/rxjs-interop para conectar signals y observables.
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Convert signal to observable for RxJS operators
private query$ = toObservable(this.query);
// RxJS pipeline with debounce, then back to signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignal se suscribe cuando se crea y se desuscribe cuando el componente se destruye. La opción initialValue elimina el tipo undefined del signal.
Preguntas de Entrevista: Angular Signals
Las entrevistas técnicas en 2026 frecuentemente evalúan la comprensión de signals. Estos son los patrones que distinguen a los candidatos senior.
P: ¿Qué sucede si un signal computed lanza un error?
El error se propaga a cualquier effect o computed que lo lea. Angular no almacena errores en caché, el cálculo reintenta en la próxima lectura. El código de producción debe manejar errores dentro de la función computed.
P: ¿Se puede escribir en un signal dentro de un computed?
No. Intentar llamar .set() o .update() dentro de un computed lanza un error. Las funciones computed deben ser puras. Se debe usar linkedSignal si se necesita un valor derivado que también sea modificable.
P: ¿Cómo manejan los effects las lecturas de signals en condicionales?
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails is only tracked when showDetails is true
console.log('Details:', userDetails().email);
}
});Los effects rastrean dependencias basándose en la ejecución más reciente. Si showDetails es false, los cambios a userDetails no disparan el effect.
P: ¿Cómo funciona la igualdad en signals?
Por defecto, los signals usan Object.is para la igualdad. Para objetos, esto significa igualdad de referencia. Se puede proporcionar igualdad personalizada:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Los candidatos que mencionan que los signals computed son memoizados y evaluados de manera perezosa demuestran comprensión más allá del uso básico de la API. Discutir cuándo usar linkedSignal vs computed muestra familiaridad con los patrones de Angular 19+.
Detección de Cambios: Signals y Angular Zoneless
Angular 22 usa por defecto detección de cambios zoneless para nuevos proyectos. Los signals son centrales a esto: notifican a Angular precisamente cuándo el estado cambia, eliminando la necesidad de que Zone.js parchee APIs asíncronas.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Signal update triggers change detection
// No Zone.js required
this.count.update(c => c + 1);
}
}Con signals, Angular rastrea exactamente qué componentes dependen de qué estado. Una actualización de signal programa detección de cambios solo para componentes afectados, no el árbol completo.
API Resource: Datos Asíncronos como Signals
La API Resource de Angular 20 (resource() y rxResource()) carga datos asíncronos de manera declarativa en signals, reemplazando patrones de suscripción manual.
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Declarative async resource
// Refetches when userId changes
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}El resource rastrea automáticamente el estado de carga, errores y el valor resuelto. Se recarga cuando el signal de petición cambia.
Mejores Prácticas para Arquitectura Signal
Estructurar signals en aplicaciones grandes requiere disciplina. Estos patrones escalan desde componentes hasta servicios.
Colocalizar signals relacionados: Agrupar signals que cambian juntos. Un componente de formulario mantiene sus signals de campos juntos, no dispersos en servicios.
Elevar estado compartido a servicios: Cuando múltiples componentes necesitan el mismo estado, mover el signal a un servicio inyectable:
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal to consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}Preferir computed sobre effects: Si un valor puede derivarse, usar computed. Los effects son para efectos secundarios que salen del grafo reactivo: manipulación del DOM, localStorage, analytics, peticiones de red.
Migrando de RxJS a Signals
Las bases de código Angular legacy pueden migrar incrementalmente. Las utilidades de interoperabilidad RxJS permiten que signals y observables coexistan.
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// After: Signal-based
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Para operaciones asíncronas, mantener RxJS para el pipeline y convertir en la frontera:
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Preparación para Preguntas de Entrevista sobre Angular Signals
- Los signals son primitivas síncronas; RxJS maneja streams asíncronos
- Los signals computed son perezosos y memoizados, recalculando solo cuando las dependencias cambian
- Los effects se ejecutan asíncronamente durante la detección de cambios, no inmediatamente
- linkedSignal resuelve el problema de estado derivado modificable sin effects
- Los inputs signal (
input(),model()) reemplazan decoradores con alternativas reactivas toSignalytoObservableconectan signals y RxJS para migración incremental- Angular zoneless depende de signals para detección de cambios granular
- La API Resource (
rxResource) gestiona estado asíncrono de manera declarativa - La guía oficial de signals de Angular sigue siendo la referencia autoritativa para detalles de la API y mejores prácticas
¿Sabrías detectar el bug en Angular?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 17 de septiembre de 2026
Etiquetas
Compartir
Artículos relacionados

Preguntas de entrevista Angular 19: Signals, SSR y conceptos imprescindibles
Las preguntas de entrevista Angular 19 más frecuentes: Signals, hidratación incremental, detección de cambios sin Zone.js y nuevas APIs reactivas con ejemplos de código y respuestas esperadas.

Inyección de Dependencias Avanzada en Angular 2026: Providers, Tokens y Preguntas de Entrevista
Dominar la inyección de dependencias en Angular con InjectionToken, inyectores jerárquicos, modificadores de resolución y multi-providers. Incluye preguntas de entrevista y patrones de producción.

Sintaxis de flujo de control en Angular 2026: @if, @for, @switch y preguntas de entrevista
Guía completa sobre la sintaxis de flujo de control de Angular con @if, @for y @switch. Aprende a reemplazar las directivas estructurales por bloques de control modernos y prepárate para entrevistas técnicas.