🇺🇸 EN
Angular
·6 min read·↗·🇺🇸 EN

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

Portada principal del artículo: 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 📊

EscenarioPatrón recomendadoPrimitiva moderna en Angular
De padre a hijoFlujo unidireccional de datosinput() o input.required()
De hijo a padreNotificación con evento personalizadooutput()
Sincronización bidireccionalSincronización mutua de estadomodel()
Componentes no relacionados o hermanosServicio singleton compartidoServicio con signal() y computed()
Padre accediendo a métodos del hijoReferencia directa al componenteSignal viewChild()

Resumen 🛠️

Para escribir aplicaciones en Angular limpias y fáciles de mantener:

  1. Usa input() y output() para la comunicación habitual de padre a hijo.
  2. Usa model() para la sincronización bidireccional de estado sin la ceremonia de emparejar @Input() y @Output().
  3. 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.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

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

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.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.