Cómo compartir datos entre componentes en Angular (Signals y servicios)

Al construir aplicaciones en Angular, compartir datos entre componentes es una necesidad constante. Ya sea que necesites pasar un elemento a una tarjeta hija, notificar a un componente padre cuando el usuario hace clic en un botón, o sincronizar el estado de un carrito de compras entre componentes sin relación directa como el header y el sidebar, Angular ofrece distintos patrones de comunicación.
⚡ ¿Usas Angular moderno (v17+)? Salta directamente a Angular moderno: Signals
input(),output(),model()y servicios con Signals para ver el enfoque idiomático y sin fugas de memoria.
En esta guía, vamos a comparar los patrones clásicos (@Input, @Output, BehaviorSubject) con las primitivas modernas de Angular (input(), output(), model() y servicios con Signals).
Empecemos con la comunicación directa de padre a hijo y de hijo a padre.
1. Comunicación de padre a hijo y de hijo a padre 🔄
Cuando dos componentes tienen una relación jerárquica directa, pasar datos hacia abajo y emitir eventos hacia arriba es el camino más limpio.
Angular clásico (v2 a v16): decoradores @Input() y @Output()
En versiones anteriores de Angular, usábamos decoradores y EventEmitter:
// child.component.ts (Clásico)
import { Component, Input, Output, EventEmitter } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ product?.name }}</h3>
<p>\${{ product?.price }}</p>
<button (click)="selectProduct()">Select</button>
</div>
`
})
export class ProductCardComponent {
@Input() product?: Product;
@Output() selected = new EventEmitter<Product>();
selectProduct() {
if (this.product) {
this.selected.emit(this.product);
}
}
}Uso en la plantilla del componente padre:
<app-product-card
[product]="currentProduct"
(selected)="onProductSelected($event)"
/>Ahora veamos cómo Angular moderno simplifica esto con Signals.
2. Angular moderno (v17+): input(), output() y model() ⚡
Angular moderno reemplaza los decoradores de propiedades con funciones reactivas basadas en Signals.
Componente hijo moderno con inputs y outputs basados en Signals
// child.component.ts (Standalone moderno)
import { Component, input, output } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="product-card">
<h3>{{ product().name }}</h3>
<p>\${{ product().price }}</p>
<button (click)="selectProduct()">Select</button>
</div>
`
})
export class ProductCardComponent {
// 1. Signal input requerido (con tipado seguro en tiempo de compilación)
readonly product = input.required<Product>();
// 2. Función output (sin el código repetitivo de instanciar EventEmitter)
readonly selected = output<Product>();
selectProduct() {
this.selected.emit(this.product());
}
}Enlace bidireccional (Two-Way Binding) con model() (Angular 17.2+)
Si necesitas enlace bidireccional entre el padre y el hijo (por ejemplo, el estado abierto de un panel o un contador de cantidad), usa model():
// quantity-picker.component.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-quantity-picker',
standalone: true,
template: `
<div class="flex gap-2">
<button (click)="quantity.set(quantity() - 1)">-</button>
<span>{{ quantity() }}</span>
<button (click)="quantity.set(quantity() + 1)">+</button>
</div>
`
})
export class QuantityPickerComponent {
// Signal bidireccional de escritura
readonly quantity = model(1);
}Uso en la plantilla del padre:
<!-- Sincroniza automáticamente en ambas direcciones -->
<app-quantity-picker [(quantity)]="itemCount" />Ahora bien, ¿qué pasa si los componentes son hermanos o no están conectados directamente en la jerarquía de la plantilla? Exploremos los servicios compartidos.
3. Comunicación entre componentes con servicios 🌐
Cuando los componentes viven en ramas separadas de tu aplicación o a través de diferentes router outlets, pasar inputs a lo largo de múltiples niveles (prop drilling) se vuelve insostenible. Un servicio inyectable resuelve esto de forma limpia.
Enfoque A: Servicio clásico con BehaviorSubject (RxJS)
// cart.service.ts (RxJS clásico)
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items$ = new BehaviorSubject<Product[]>([]);
readonly currentItems$ = this.items$.asObservable();
addItem(product: Product) {
const updated = [...this.items$.value, product];
this.items$.next(updated);
}
}En los componentes, tenías que gestionar las suscripciones con el pipe async o desuscribirte manualmente para evitar fugas de memoria:
<header>Cart Items: {{ (cartService.currentItems$ | async)?.length }}</header>Enfoque B: Servicio de estado moderno con Signals (sin fugas y sin pipe async)
En Angular moderno, los servicios con Signals proporcionan un estado síncrono y fácil de leer, sin necesidad de gestionar suscripciones:
// cart.service.ts (Estado moderno con Signals)
import { Injectable, signal, computed } from '@angular/core';
import { Product } from './product.model';
@Injectable({ providedIn: 'root' })
export class CartService {
// 1. Signal privada de escritura
private readonly itemsSignal = signal<Product[]>([]);
// 2. Signal pública de solo lectura
readonly items = this.itemsSignal.asReadonly();
// 3. Estado derivado con computed (se recalcula automáticamente cuando items cambia)
readonly totalCount = computed(() => this.items().length);
readonly totalPrice = computed(() =>
this.items().reduce((acc, item) => acc + item.price, 0)
);
addItem(product: Product) {
this.itemsSignal.update((current) => [...current, product]);
}
removeItem(productId: number) {
this.itemsSignal.update((current) => current.filter((p) => p.id !== productId));
}
}Consumir el servicio con Signals en cualquier componente
// cart-badge.component.ts
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-cart-badge',
standalone: true,
template: `
<div class="badge">
🛒 {{ cart.totalCount() }} items (\${{ cart.totalPrice() }})
</div>
`
})
export class CartBadgeComponent {
readonly cart = inject(CartService);
}4. Consultar componentes hijos directos con viewChild() 🔍
Cuando un componente padre necesita acceso directo a los métodos o propiedades públicas de un componente hijo:
import { Component, viewChild, AfterViewInit } from '@angular/core';
import { ProductCardComponent } from './product-card.component';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [ProductCardComponent],
template: `<app-product-card #card [product]="featuredProduct" />`
})
export class ProductListComponent {
// viewChild moderno basado en Signals (reemplaza al decorador @ViewChild)
readonly card = viewChild.required<ProductCardComponent>('card');
focusCard() {
console.log('Accediendo a la instancia del componente hijo:', this.card());
}
}Matriz de comunicación: cuándo usar cada patrón 📊
| Escenario | Patrón recomendado | Primitiva moderna en Angular |
|---|---|---|
| De padre a hijo | Flujo unidireccional de datos | input() o input.required() |
| De hijo a padre | Notificación con evento personalizado | output() |
| Sincronización bidireccional | Sincronización mutua de estado | model() |
| Componentes no relacionados o hermanos | Servicio singleton compartido | Servicio con signal() y computed() |
| Padre accediendo a métodos del hijo | Referencia directa al componente | Signal viewChild() |
Resumen 🛠️
Para escribir aplicaciones en Angular limpias y fáciles de mantener:
- Usa
input()youtput()para la comunicación habitual de padre a hijo. - Usa
model()para la sincronización bidireccional de estado sin la ceremonia de emparejar@Input()y@Output(). - Usa servicios con Signals (
signal(),asReadonly(),computed()) para compartir estado global y entre componentes sin fugas de memoria.
Si quieres conocer más patrones de arquitectura moderna en Angular, revisa mis artículos sobre Cómo simplificar la comunicación entre componentes con model() y Signals y Cómo usar ng-template, ng-container y ng-content en Angular.
These are my experiences learning and facing my daily challenges working with Angular.
Preguntas Frecuentes
¿Cuál es la diferencia entre el @Input() clásico y el input() con Signals moderno en Angular?
El @Input() clásico es un decorador de propiedad que depende de Zone.js y de los hooks del ciclo de vida del componente para detectar cambios. El input() moderno con Signals (disponible desde Angular 17.1+) devuelve una Signal reactiva que se integra directamente con signals computed(), asegura tipado estricto con input.required() y elimina problemas con ChangeDetectionStrategy.OnPush.
¿Cómo se comparten datos con enlace bidireccional entre componentes padre e hijo en Angular moderno?
Usa la función model() (introducida en Angular 17.2+). Crea una signal de escritura en el componente hijo que se sincroniza automáticamente con un enlace bidireccional [(propiedad)] en el padre, eliminando la necesidad de coordinar manualmente un @Input() con un EventEmitter en @Output().
¿Cuándo debería usar un servicio en lugar de Input/Output para compartir datos en Angular?
Usa Input/Output (o input/output con Signals) para la comunicación directa entre padre e hijo. Cuando los componentes son hermanos, están anidados profundamente a través de múltiples router outlets o necesitan compartir estado global en la aplicación, usa un servicio inyectable con Signals o RxJS.
¿Cómo reemplazan los servicios de estado basados en Signals a los servicios con BehaviorSubject en Angular moderno?
En lugar de crear un BehaviorSubject privado con un flujo asObservable() y gestionar suscripciones manuales, creas una signal() privada y la expones como una signal de solo lectura con asReadonly(). Esto proporciona a los componentes una reactividad síncrona y sin fugas de memoria, sin necesidad de usar el pipe async ni lógica para cancelar suscripciones.
Artículos Relacionados
Cómo simplificar la comunicación entre componentes en Angular con model() y Signals
Estuve muy ocupado en septiembre, pero ahora estoy preparando un taller sobre signals y explorando ideas sobre cómo pueden simplificar nuestro código. Recientemente usaba @Input() y @Output para la comunicación entre componentes, pero Angular 17 introdujo alternativas más limpias...
Cómo usar ng-template, ng-container y ng-content en Angular (Guía completa)
Aprende las diferencias exactas entre ng-template, ng-container y ng-content en Angular, cómo pasar contexto con ngTemplateOutlet, proyección con múltiples slots y cómo evolucionan con el control flow moderno de @if, @for y @defer.
Cómo usar parámetros de ruta y query params en Angular (con Signals e Inputs)
Aprende a pasar y leer parámetros de ruta, query params y parámetros de matriz en Angular usando Signals, withComponentInputBinding y ActivatedRoute.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.