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.