Comunicación padre-hijo con input()

Intermedio
Angular
Angular
Actualizado: 04/05/2026

Introducción a comunicación padre-hijo

flowchart TD
    Root[AppComponent root] --> Header[Header]
    Root --> Main[Main]
    Root --> Footer[Footer]
    Main --> Sidebar[Sidebar]
    Main --> Content[Content]
    Content --> List[ProductList]
    List --> Card1[ProductCard 1]
    List --> Card2[ProductCard 2]
    List --> CardN[ProductCard N]
    Root -.input signal.-> Header
    List -.input product.-> Card1
    Card1 -.output buy.-> List
    List -.output filter.-> Content
    Content -.event bus<br/>service signal.-> Sidebar
    style Root fill:#fff3e0,stroke:#ef6c00
    style Card1 fill:#e3f2fd,stroke:#1565c0

En aplicaciones Angular reales, raramente trabajamos con un único componente aislado. En su lugar, construimos interfaces de usuario complejas dividiendo la funcionalidad en múltiples componentes que cooperan entre sí. Esta arquitectura modular hace que nuestras aplicaciones sean más mantenibles y reutilizables.

La comunicación entre componentes es fundamental para crear aplicaciones Angular efectivas. Los componentes necesitan compartir datos, notificarse sobre eventos y coordinar su comportamiento para ofrecer una experiencia de usuario cohesiva.

Jerarquía de componentes

Angular organiza los componentes en una estructura jerárquica similar a un árbol familiar. Cuando un componente incluye otro componente en su plantilla, establece una relación padre-hijo:

<!-- Componente padre: AppComponent -->
<div class="app-container">
  <app-header></app-header>
  <app-product-list></app-product-list>
  <app-footer></app-footer>
</div>

En este ejemplo, AppComponent actúa como componente padre de HeaderComponent, ProductListComponent y FooterComponent, que son sus componentes hijos.

Patrones de comunicación

Existen varios patrones para la comunicación entre componentes en Angular:

  • Comunicación padre -> hijo: El padre envía datos al hijo (usando input() o @Input)
  • Comunicación hijo -> padre: El hijo notifica eventos al padre (usando output() o @Output)
  • Comunicación entre hermanos: A través del padre común o servicios compartidos
  • Comunicación global: Usando servicios con estado compartido

Comunicación padre-hijo en detalle

La comunicación padre-hijo es el patrón más común y fundamental. Permite que el componente padre controle y configure el comportamiento de sus componentes hijos enviándoles datos.

Casos de uso típicos:

  • Pasar configuración a componentes reutilizables
  • Enviar datos dinámicos que el hijo debe mostrar
  • Controlar el estado o apariencia de componentes hijos
  • Personalizar el comportamiento de componentes genéricos

Imaginemos un componente ProductCard que debe mostrar información de diferentes productos:

// Componente padre
@Component({
  selector: 'app-product-list',
  template: `
    <div class="product-grid">
      <app-product-card></app-product-card>
      <app-product-card></app-product-card>
      <app-product-card></app-product-card>
    </div>
  `
})
export class ProductListComponent {
  // ¿Cómo le decimos a cada tarjeta qué producto mostrar?
}

Sin comunicación padre-hijo, cada ProductCard sería idéntica y estática. Necesitamos una forma de que el padre envíe datos específicos a cada instancia del componente hijo.

Beneficios del patrón padre-hijo

Este patrón de comunicación ofrece múltiples ventajas arquitectónicas:

  • Reutilización: Un componente hijo puede ser usado en diferentes contextos
  • Separación de responsabilidades: El padre gestiona los datos, el hijo se encarga de la presentación
  • Mantenibilidad: Cambios en la lógica del padre no afectan la implementación del hijo
  • Testabilidad: Componentes independientes son más fáciles de probar

Flujo de datos unidireccional

Angular implementa un flujo de datos unidireccional donde los datos fluyen desde componentes padre hacia componentes hijos. Este patrón hace que el comportamiento de la aplicación sea más predecible y fácil de depurar.

Los datos siempre van "hacia abajo" en la jerarquía, desde el padre hacia los hijos, mientras que los eventos van "hacia arriba", desde los hijos hacia el padre. Esta consistencia direccional evita bucles de dependencias y hace más sencillo rastrear el origen de los cambios en la aplicación.

Preparación para input()

Para implementar comunicación padre-hijo, Angular 21 proporciona la función input() basada en signals como enfoque principal. Esta función permite que un componente hijo declare qué datos puede recibir de su padre, devolviendo un signal de solo lectura que se integra perfectamente con el sistema de reactividad de Angular.

También existe el decorador clásico @Input que sigue funcionando en aplicaciones existentes. En las siguientes secciones veremos primero el enfoque moderno con input() y después el enfoque clásico con @Input.

Enfoque moderno: input()

La función input() es la forma recomendada en Angular 21 para declarar inputs en componentes. Forma parte del sistema de signals y devuelve un InputSignal de solo lectura que se actualiza automáticamente cuando el componente padre envía nuevos valores.

Sintaxis básica de input()

Para declarar un input, usamos la función input() importada desde @angular/core:

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-product-card',
  template: `
    <div class="product-card">
      <h3>{{ productName() }}</h3>
      <p>Precio: {{ price() }}€</p>
    </div>
  `
})
export class ProductCardComponent {
  productName = input<string>('');
  price = input<number>(0);
}

Observa que en el template se accede al valor llamando al signal como una función: {{ productName() }}. Con esta declaración, nuestro componente ProductCard puede recibir valores para productName y price desde su componente padre.

Enviando datos desde el padre

El componente padre utiliza property binding con corchetes para enviar datos a los inputs del hijo, exactamente igual que con @Input:

@Component({
  selector: 'app-product-list',
  imports: [ProductCardComponent],
  template: `
    <div class="product-grid">
      <app-product-card 
        [productName]="'iPhone 16'"
        [price]="999">
      </app-product-card>
      
      <app-product-card 
        [productName]="'MacBook Pro'"
        [price]="2499">
      </app-product-card>
    </div>
  `
})
export class ProductListComponent { }

Datos dinámicos con propiedades del padre

En aplicaciones reales, los datos suelen venir de propiedades dinámicas del componente padre en lugar de valores estáticos:

@Component({
  selector: 'app-product-list',
  imports: [ProductCardComponent],
  template: `
    <div class="product-grid">
      @for (product of products; track product.id) {
        <app-product-card 
          [productName]="product.name"
          [price]="product.price">
        </app-product-card>
      }
    </div>
  `
})
export class ProductListComponent {
  products = [
    { id: 1, name: 'iPhone 16', price: 999 },
    { id: 2, name: 'MacBook Pro', price: 2499 },
    { id: 3, name: 'iPad Air', price: 649 }
  ];
}

Inputs requeridos con input.required()

Para declarar que un input es obligatorio y el padre debe proporcionarlo, usamos input.required():

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `
    <div class="user-profile">
      <h2>{{ user().name }}</h2>
      <p>Activo: {{ isActive() ? 'Sí' : 'No' }}</p>
      <ul>
        @for (skill of skills(); track skill) {
          <li>{{ skill }}</li>
        }
      </ul>
    </div>
  `
})
export class UserProfileComponent {
  // Input requerido - el padre debe proporcionarlo
  user = input.required<{ name: string; email: string }>();
  
  // Inputs opcionales con valor por defecto
  isActive = input<boolean>(false);
  skills = input<string[]>([]);
}

Si el padre no proporciona un input.required(), Angular lanzará un error en tiempo de ejecución, lo que facilita la detección de errores.

Alias de inputs

Podemos definir un alias para que el nombre público del input sea diferente del nombre interno:

export class ProductCardComponent {
  // El padre usa [productTitle], internamente se llama internalName
  internalName = input<string>('', { alias: 'productTitle' });
}
<app-product-card [productTitle]="product.name"></app-product-card>

Integración con computed() y effect()

Una de las grandes ventajas de input() es su integración nativa con el sistema de signals. Podemos crear valores derivados con computed() o ejecutar efectos con effect():

import { Component, input, computed, effect } from '@angular/core';

@Component({
  selector: 'app-product-card',
  template: `
    <div class="product-card">
      <h3>{{ productName() }}</h3>
      <p>Precio: {{ price() }}€</p>
      <p>Precio con IVA: {{ precioConIva() }}€</p>
    </div>
  `
})
export class ProductCardComponent {
  productName = input.required<string>();
  price = input<number>(0);
  
  // Valor derivado que se recalcula automáticamente
  precioConIva = computed(() => (this.price() * 1.21).toFixed(2));
  
  constructor() {
    // Efecto que reacciona a cambios en el input
    effect(() => {
      console.log(`Precio actualizado: ${this.price()}€`);
    });
  }
}

Buenas prácticas con input()

Al trabajar con input(), es recomendable seguir estas buenas prácticas:

  • Usar input.required() para inputs obligatorios en lugar de aserción no nula
  • Tipar correctamente: Siempre específica el tipo genérico input<string>()
  • Valores por defecto: Proporciona valores sensatos para inputs opcionales
  • Usar computed() para valores derivados en lugar de cálculos en el template
  • Inmutabilidad: Los InputSignal son de solo lectura, lo que refuerza la inmutabilidad

Enfoque clásico: @Input

El decorador @Input es el enfoque clásico que sigue funcionando en Angular 21 y es ampliamente utilizado en aplicaciones existentes. Permite que un componente hijo declare propiedades que pueden recibir datos desde su componente padre.

Sintaxis básica de @Input

Para declarar una propiedad como entrada, aplicamos el decorador @Input() antes de la declaración de la propiedad:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-product-card',
  template: `
    <div class="product-card">
      <h3>{{ productName }}</h3>
      <p>Precio: {{ price }}€</p>
    </div>
  `
})
export class ProductCardComponent {
  @Input() productName: string = '';
  @Input() price: number = 0;
}

Propiedades requeridas y opcionales

Por defecto, todas las propiedades @Input son opcionales. Podemos hacerlas requeridas usando la opción required:

export class ProductCardComponent {
  // Propiedad requerida
  @Input({ required: true }) productName!: string;
  
  // Propiedad opcional con valor por defecto
  @Input() price: number = 0;
  @Input() showDiscount: boolean = false;
}

Alias de propiedades

export class ProductCardComponent {
  @Input('productTitle') internalName: string = '';
}

Validación básica en el setter

Podemos usar setters para validar o procesar los datos que llegan a través de @Input:

export class ProductCardComponent {
  private _price: number = 0;
  
  @Input() 
  set price(value: number) {
    this._price = value < 0 ? 0 : value;
  }
  
  get price(): number {
    return this._price;
  }
}

Comparativa: input() vs @Input

| Característica | input() (moderno) | @Input (clásico) | |---|---|---| | Tipo de valor | InputSignal (solo lectura) | Propiedad mutable | | Acceso en template | {{ miInput() }} | {{ miInput }} | | Inputs requeridos | input.required<T>() | @Input({ required: true }) | | Integración con signals | Nativa (computed, effect) | Requiere conversión manual | | Detección de cambios | Reactiva con signals | Basada en ngOnChanges |

Se recomienda usar input() en código nuevo, pero @Input sigue siendo completamente válido y no requiere migración inmediata en aplicaciones existentes.

Inputs avanzados: transforms

Las funciones de transformación permiten procesar automáticamente los datos antes de asignarlos al input del componente. Esta característica funciona tanto con input() como con @Input y elimina la necesidad de lógica de conversión manual.

Transforms con input()

La función input() acepta una opción transform que procesa el valor antes de almacenarlo en el signal:

import { Component, input, booleanAttribute, numberAttribute } from '@angular/core';

@Component({
  selector: 'app-config-panel',
  template: `
    <div class="config-panel">
      <p>Modo debug: {{ debugMode() ? 'Activado' : 'Desactivado' }}</p>
      <p>Número de elementos: {{ itemCount() }}</p>
    </div>
  `
})
export class ConfigPanelComponent {
  debugMode = input(false, { transform: booleanAttribute });
  itemCount = input(0, { transform: numberAttribute });
}

Transformaciones incorporadas

Angular proporciona funciones de transformación predefinidas para casos comunes:

booleanAttribute - Convierte strings y valores truthy a booleanos:

export class ToggleComponent {
  // Enfoque moderno con input()
  enabled = input(false, { transform: booleanAttribute });
}
<!-- Todos estos se convierten a true -->
<app-toggle enabled></app-toggle>
<app-toggle enabled="true"></app-toggle>
<app-toggle enabled=""></app-toggle>
<app-toggle [enabled]="someVariable"></app-toggle>

numberAttribute - Convierte strings a números:

export class CounterComponent {
  // Enfoque moderno con input()
  initialValue = input(0, { transform: numberAttribute });
}
<!-- Se convierte automáticamente a number -->
<app-counter initialValue="42"></app-counter>
<app-counter [initialValue]="stringNumber"></app-counter>

Funciones de transformación personalizadas

Podemos crear transformaciones personalizadas para casos específicos de nuestra aplicación:

// Función para convertir texto a mayúsculas
function uppercaseTransform(value: string): string {
  return value?.toString().toUpperCase() || '';
}

// Función para formatear precios
function priceTransform(value: number): string {
  return new Intl.NumberFormat('es-ES', {
    style: 'currency',
    currency: 'EUR'
  }).format(value || 0);
}

@Component({
  selector: 'app-product-display',
  template: `
    <div class="product">
      <h3>{{ title() }}</h3>
      <p class="price">{{ formattedPrice() }}</p>
    </div>
  `
})
export class ProductDisplayComponent {
  title = input('', { transform: uppercaseTransform });
  formattedPrice = input('', { transform: priceTransform });
}

Transformaciones con múltiples parámetros

Las funciones de transformación pueden ser más complejas y manejar diferentes tipos de entrada:

function trimAndCapitalize(value: string | null | undefined): string {
  if (!value) return '';
  
  const trimmed = value.toString().trim();
  return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
}

function clampNumber(min: number, max: number) {
  return (value: number): number => {
    const num = Number(value) || 0;
    return Math.min(Math.max(num, min), max);
  };
}

export class UserInputComponent {
  userName = input('', { transform: trimAndCapitalize });
  percentage = input(0, { transform: clampNumber(0, 100) });
}

Transformaciones con arrays y objetos

También podemos transformar estructuras de datos complejas:

function parseTagsFromString(value: string | string[]): string[] {
  if (Array.isArray(value)) return value;
  if (!value) return [];
  
  return value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
}

function normalizeUser(value: any): { name: string; email: string } {
  return {
    name: value?.name || 'Usuario desconocido',
    email: value?.email || 'email@ejemplo.com'
  };
}

export class TaggedContentComponent {
  tags = input<string[]>([], { transform: parseTagsFromString });
  user = input.required<{ name: string; email: string }>({ transform: normalizeUser });
}

Combinando transforms con validación

Las transformaciones pueden incluir lógica de validación para garantizar datos consistentes:

function validateAndParseDate(value: string | Date): Date {
  if (value instanceof Date) return value;
  
  const parsed = new Date(value);
  if (isNaN(parsed.getTime())) {
    console.warn(`Fecha inválida recibida: ${value}`);
    return new Date(); // Fecha actual por defecto
  }
  
  return parsed;
}

function sanitizeHtml(value: string): string {
  return value
    ?.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    ?.replace(/<[^>]*>/g, '') || '';
}

export class ContentComponent {
  publishDate = input(new Date(), { transform: validateAndParseDate });
  description = input('', { transform: sanitizeHtml });
}

Ventajas de las transformaciones

El uso de transform functions ofrece múltiples beneficios:

  • Automatización: Las transformaciones se aplican automáticamente sin código adicional
  • Reutilización: Las funciones de transformación se pueden compartir entre componentes
  • Consistencia: Garantizan que los datos siempre lleguen en el formato esperado
  • Simplicidad: Eliminan la necesidad de setters personalizados complejos
  • Performance: Son más eficientes que las transformaciones manuales en templates

Casos de uso prácticos

Las transformaciones son especialmente útiles en estos escenarios comunes:

  • Formularios: Normalizar entrada de usuario (trim, mayúsculas, formato)
  • APIs: Convertir formatos de datos entre frontend y backend
  • Componentes de UI: Estandarizar propiedades de configuración
  • Validación: Asegurar tipos y formatos correctos
  • Localización: Adaptar datos a formatos locales

Consideraciones de rendimiento

Las funciones de transformación deben ser funciones puras sin efectos secundarios para garantizar un comportamiento predecible:

// ✅ Correcto - Función pura
function formatCurrency(value: number): string {
  return new Intl.NumberFormat('es-ES', {
    style: 'currency',
    currency: 'EUR'
  }).format(value || 0);
}

// ❌ Incorrecto - Tiene efectos secundarios
function logAndFormat(value: number): string {
  console.log('Valor recibido:', value); // Efecto secundario
  return formatCurrency(value);
}

Las transform functions representan una herramienta avanzada que mejora significativamente la robustez y mantenibilidad de nuestros componentes, proporcionando un mecanismo elegante para garantizar que los datos siempre lleguen en el formato correcto.

Transforms con el enfoque clásico @Input

Las transformaciones también funcionan con el decorador @Input clásico, usando la misma sintaxis de opciones:

export class ConfigPanelComponent {
  @Input({ transform: booleanAttribute }) debugMode: boolean = false;
  @Input({ transform: numberAttribute }) itemCount: number = 0;
}

La funcionalidad de transforms es idéntica en ambos enfoques; la diferencia es que con input() el resultado es un signal reactivo.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Angular

Documentación oficial de Angular
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender la arquitectura de comunicación padre-hijo en Angular 21.
  • Aprender a usar la función input() basada en signals como enfoque principal para recibir datos del componente padre.
  • Conocer cómo enviar datos estáticos y dinámicos mediante property binding.
  • Explorar el uso de input.required() y opciones de transformación.
  • Conocer el enfoque clásico con @Input que sigue funcionando en aplicaciones existentes.
  • Aplicar buenas prácticas en la definición y uso de inputs para componentes reutilizables.