DEV Community

Cover image for Angular Component Communication: Complete Guide | @Input @Output EventEmitter & ViewChild
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Component Communication: Complete Guide | @Input @Output EventEmitter & ViewChild

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.

📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What is Angular Component Communication?

Angular Component Communication provides multiple patterns:

  • @Input - Pass data from parent to child (one-way data flow)
  • @Output - Emit events from child to parent
  • ViewChild/ViewChildren - Access child components directly
  • ContentChild/ContentChildren - Access projected content
  • Services with RxJS - Share data between any components
  • Route Data - Pass data through navigation

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.

// 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>
Enter fullscreen mode Exit fullscreen mode

@Input with Setter

React to input changes using setters:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

@Input with ngOnChanges

Detect and react to input changes:

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Child to Parent: @Output and EventEmitter

Emit events from child to parent using @Output with EventEmitter:

// 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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Emitting Complex Data

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>
Enter fullscreen mode Exit fullscreen mode

ViewChild and ViewChildren

Access child components from parent using ViewChild and ViewChildren:

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();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

ViewChild with Template Reference

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>
Enter fullscreen mode Exit fullscreen mode

ContentChild and ContentChildren

Access projected content using ContentChild and ContentChildren:

// 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Service-Based Communication

Share data between any components using 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Subject for Event Broadcasting

@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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Choosing the Right Pattern

Here's my decision framework for choosing communication patterns:

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.

Need to Access Projected Content?

Use ContentChild or ContentChildren. For accessing content projected via <ng-content>.

Advanced Patterns

Combining Multiple Patterns

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' });
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Route Data

// 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use @Input for one-way data flow - From parent to child
  2. Use @Output with EventEmitter - For child to parent communication
  3. Use services with RxJS - For sibling component communication
  4. Use ViewChild/ViewChildren - To access child component APIs
  5. Avoid direct component references - When possible, prefer @Input/@Output
  6. Use BehaviorSubject for shared state - Maintains current value
  7. Unsubscribe from observables - Prevent memory leaks
  8. Keep communication simple and explicit - Avoid over-engineering
  9. Use OnPush change detection - When appropriate for performance
  10. Document component inputs and outputs - Clear API documentation

Memory Leak Prevention

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Patterns

Parent-Child Communication Flow

Parent Component
    ↓ [@Input]
Child Component
    ↓ [@Output]
Parent Component
Enter fullscreen mode Exit fullscreen mode

Sibling Communication Flow

Component A
    ↓ [Service]
Shared Service (RxJS)
    ↓ [Observable]
Component B
Enter fullscreen mode Exit fullscreen mode

Component Hierarchy Communication

Grandparent
    ↓ [@Input]
Parent
    ↓ [@Input]
Child
    ↑ [@Output]
Parent
    ↑ [@Output]
Grandparent
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

Conclusion

Angular Component Communication provides multiple patterns for sharing data between components. Understanding when to use each pattern is crucial for building maintainable Angular applications.

Key Takeaways:

  • @Input/@Output - For parent-child communication
  • ViewChild/ViewChildren - For accessing child components
  • ContentChild/ContentChildren - For accessing projected content
  • Services with RxJS - For sibling and app-wide communication
  • Choose the simplest pattern - Start with @Input/@Output
  • Prevent memory leaks - Always unsubscribe from observables
  • Keep it simple - Avoid over-engineering communication

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. Keep it simple, and your future self will thank you.


What's your experience with Angular Component Communication? Share your tips and tricks in the comments below! 🚀


💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.

Top comments (0)