Back to Blog
Angular18 min read

Angular Component Communication – Complete Guide

Learn Angular component communication patterns with @Input, @Output, EventEmitter, ViewChild, ContentChild, services, and RxJS. Complete guide for parent-child communication.

When I first started working with Angular, component communication was one of the most confusing aspects of the framework. I'd find myself passing data through multiple levels of components, creating complex event chains, or resorting to global state when a simpler solution existed. It took building several applications before I understood when to use each communication pattern.

The truth is, Angular gives you many ways to share data between components, and each has its place. @Input and @Output are perfect for parent-child relationships. ViewChild lets you access child components directly. Services with RxJS are ideal for sibling communication or sharing state across the app. Understanding which pattern to use when is crucial for building maintainable Angular applications.

In this guide, I'll walk through each communication pattern with real examples from applications I've built. I'll explain not just how they work, but when to use them and what pitfalls to avoid. By the end, you'll have a clear mental model for choosing the right communication strategy for any scenario.

Parent to Child: Using @Input

The most common communication pattern in Angular is passing data from parent to child using @Input. This is Angular's way of implementing one-way data flow, which makes your components easier to reason about and debug. I use this pattern constantly—it's the foundation of most component hierarchies.

Here's a practical example from a business management application I built. The parent component manages the business data, and the child component displays and allows editing of that data:

// Child Component import { Component, Input } from '@angular/core'; @Component({ selector: 'app-business-settings', templateUrl: './business-settings.component.html' }) export class BusinessSettingsComponent { @Input() public manageBusiness: boolean; @Input() public businessInfo: any; @Input() public viewDetails: boolean = false; } // Parent Component Template <app-business-settings [manageBusiness]="canManage" [businessInfo]="currentBusiness" [viewDetails]="isViewMode"> </app-business-settings>

@Input with Setter

React to input changes using setters. This is useful when you need to perform actions when an input value changes:

export class BusinessSettingsComponent { private _businessInfo: any; @Input() set businessInfo(value: any) { this._businessInfo = value; // React to changes if (value) { this.loadSettings(value.id); } } get businessInfo(): any { return this._businessInfo; } }

@Input with ngOnChanges

Detect and react to input changes using the ngOnChanges lifecycle hook:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; export class BusinessSettingsComponent implements OnChanges { @Input() businessInfo: any; ngOnChanges(changes: SimpleChanges): void { if (changes['businessInfo'] && !changes['businessInfo'].firstChange) { // React to businessInfo changes this.updateSettings(); } } }

Child to Parent: @Output and EventEmitter

Emit events from child to parent:

// Child Component import { Component, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-business-settings', templateUrl: './business-settings.component.html' }) export class BusinessSettingsComponent { @Output() public refresh: EventEmitter<boolean> = new EventEmitter<boolean>(); public save(): void { // Save logic this.refresh.emit(true); } public cancel(): void { this.refresh.emit(false); } } // Parent Component Template <app-business-settings (refresh)="handleRefresh($event)"> </app-business-settings> // Parent Component export class BusinessComponent { handleRefresh(refresh: boolean): void { if (refresh) { this.loadBusiness(); } } }

Emitting Complex Data

Emit complex objects or data structures from child to parent:

export class BusinessSettingsComponent { @Output() businessUpdated = new EventEmitter<Business>(); save(): void { const updatedBusiness = { id: this.businessInfo.id, name: this.form.value.name, // ... other fields }; this.businessUpdated.emit(updatedBusiness); } } // Parent <app-business-settings (businessUpdated)="onBusinessUpdated($event)"> </app-business-settings>

ViewChild and ViewChildren

Access child components from parent:

import { Component, ViewChild, ViewChildren, QueryList, AfterViewInit } from '@angular/core'; import { ChildComponent } from './child.component'; @Component({ selector: 'app-parent', template: ` <app-child #childRef></app-child> <app-child *ngFor="let item of items"></app-child> ` }) export class ParentComponent implements AfterViewInit { @ViewChild('childRef') childComponent: ChildComponent; @ViewChild(ChildComponent) firstChild: ChildComponent; @ViewChildren(ChildComponent) children: QueryList<ChildComponent>; ngAfterViewInit(): void { // Access child component methods this.childComponent.doSomething(); // Access all children this.children.forEach(child => { child.update(); }); } }

ViewChild with Template Reference

Use template reference variables to access child components:

export class ParentComponent { @ViewChild('childRef', { static: false }) childComponent: ChildComponent; callChildMethod(): void { this.childComponent.save(); } } // Template <app-child #childRef></app-child> <button (click)="callChildMethod()">Save</button>

Service-Based Communication

Share data through services with RxJS:

// Service import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class DataSharingService { private dataSubject = new BehaviorSubject<any>(null); public data$ = this.dataSubject.asObservable(); setData(data: any): void { this.dataSubject.next(data); } getData(): Observable<any> { return this.data$; } } // Component 1 (Sender) export class Component1 { constructor(private dataService: DataSharingService) {} sendData(): void { this.dataService.setData({ message: 'Hello' }); } } // Component 2 (Receiver) export class Component2 implements OnInit, OnDestroy { data: any; private subscription: Subscription; constructor(private dataService: DataSharingService) {} ngOnInit(): void { this.subscription = this.dataService.getData().subscribe(data => { this.data = data; }); } ngOnDestroy(): void { this.subscription?.unsubscribe(); } }

Using Subject for Event Broadcasting

Use Subject for event broadcasting between components:

@Injectable({ providedIn: 'root' }) export class EventBusService { private eventSubject = new Subject<any>(); public events$ = this.eventSubject.asObservable(); emit(event: any): void { this.eventSubject.next(event); } } // Component 1 export class Component1 { constructor(private eventBus: EventBusService) {} triggerEvent(): void { this.eventBus.emit({ type: 'USER_ACTION', data: 'some data' }); } } // Component 2 export class Component2 implements OnInit, OnDestroy { private subscription: Subscription; constructor(private eventBus: EventBusService) {} ngOnInit(): void { this.subscription = this.eventBus.events$.subscribe(event => { if (event.type === 'USER_ACTION') { this.handleEvent(event.data); } }); } ngOnDestroy(): void { this.subscription?.unsubscribe(); } }

ContentChild and ContentChildren

Access projected content:

// Parent Component @Component({ selector: 'app-parent', template: ` <app-wrapper> <app-child #projected></app-child> </app-wrapper> ` }) export class ParentComponent { } // Wrapper Component import { Component, ContentChild, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; @Component({ selector: 'app-wrapper', template: `<ng-content></ng-content>` }) export class WrapperComponent implements AfterContentInit { @ContentChild('projected') projectedChild: ChildComponent; @ContentChildren(ChildComponent) projectedChildren: QueryList<ChildComponent>; ngAfterContentInit(): void { this.projectedChild.doSomething(); } }

Two-Way Data Binding

Implement two-way data binding with @Input and @Output:

// Child Component export class ChildComponent { @Input() value: string; @Output() valueChange = new EventEmitter<string>(); updateValue(newValue: string): void { this.value = newValue; this.valueChange.emit(newValue); } } // Parent Component Template <app-child [(value)]="parentValue"> </app-child> // Equivalent to: <app-child [value]="parentValue" (valueChange)="parentValue = $event"> </app-child>

Advanced Patterns

Combining Multiple Patterns

You can combine multiple communication patterns in a single component:

export class ParentComponent { @ViewChild(ChildComponent) child: ChildComponent; constructor(private dataService: DataSharingService) {} // Use ViewChild to call child method triggerChildSave(): void { this.child.save(); } // Use service for sibling communication shareDataWithSiblings(): void { this.dataService.setData({ shared: 'data' }); } }

Using Route Data

Pass data through route navigation:

// Navigate with data this.router.navigate(['/details'], { state: { business: this.business } }); // Receive in component export class DetailsComponent implements OnInit { business: Business; constructor(private router: Router) {} ngOnInit(): void { this.business = history.state.business; } }

Best Practices

  • Use @Input for one-way data flow from parent to child
  • Use @Output with EventEmitter for child to parent communication
  • Use services with RxJS for sibling component communication
  • Use ViewChild/ViewChildren to access child component APIs
  • Avoid direct component references when possible
  • Use BehaviorSubject for shared state management
  • Unsubscribe from observables to prevent memory leaks
  • Keep component communication simple and explicit
  • Use OnPush change detection strategy when appropriate
  • Document component inputs and outputs clearly

Memory Leak Prevention

Always unsubscribe from observables to prevent memory leaks:

export class Component implements OnInit, OnDestroy { private subscriptions = new Subscription(); ngOnInit(): void { // Add all subscriptions this.subscriptions.add( this.dataService.getData().subscribe(data => { // Handle data }) ); } ngOnDestroy(): void { // Unsubscribe from all this.subscriptions.unsubscribe(); } }

Choosing the Right Pattern: My Decision Framework

After working with Angular for years, I've developed a simple decision framework for choosing communication patterns. Here's how I think about it:

Parent to Child? Use @Input. It's simple, performant, and follows Angular's one-way data flow principle. This covers about 70% of component communication needs.

Child to Parent? Use @Output with EventEmitter. It's explicit, type-safe, and easy to test. Perfect for user actions that need to bubble up.

Need to Access Child Methods? Use ViewChild or ViewChildren. This is useful when you need to call methods on child components or access their properties directly.

Sibling Components or App-Wide State? Use a service with RxJS. BehaviorSubject is perfect for shared state, while Subject works well for event broadcasting.

The key is to start with the simplest pattern that works (@Input/@Output), and only move to more complex solutions (services, ViewChild) when you actually need them. Over-engineering component communication is a common mistake I see in Angular codebases. Keep it simple, and your future self will thank you.