Shashikanth Kota

Angular Component Communication: Patterns and Best Practices

Published on July 15, 2023

Introduction

Communication between components is a fundamental aspect of Angular applications. As applications grow in complexity, implementing effective component communication becomes crucial for maintaining a clean architecture and ensuring data flows correctly throughout your application. This post explores various patterns and best practices for component communication in Angular.

Table of Contents

  1. Understanding Component Relationships
  2. Parent to Child: @Input Decorator
  3. Child to Parent: @Output and EventEmitter
  4. Sharing Data with Services
  5. Using RxJS Subjects and BehaviorSubjects
  6. Component Communication with NgRx
  7. ViewChild and ContentChild
  8. Component Interaction via Template Variables
  9. Communication Between Sibling Components
  10. Best Practices
  11. Common Pitfalls and Solutions
  12. Conclusion

Understanding Component Relationships

Before diving into specific techniques, it’s important to understand the different types of component relationships in Angular:

  1. Parent-Child: The most common relationship, where one component (parent) includes another (child) in its template.
  2. Siblings: Components that share the same parent but don’t directly include each other.
  3. Unrelated: Components that don’t have a direct relationship in the component tree.

The communication technique you choose should be based on the relationship between the components and the specific requirements of your application.

Parent to Child: @Input Decorator

The simplest form of component communication is passing data from a parent component to a child component using the @Input decorator.

Parent Component

// parent.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <div>
      <label for="message-input">Message to child: </label>
      <input id="message-input" [(ngModel)]="parentMessage">
    </div>
    <app-child [message]="parentMessage"></app-child>
  `
})
export class ParentComponent {
  parentMessage = 'Message from parent';
}

Child Component

// child.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div class="child">
      <h3>Child Component</h3>
      <p>Message from parent: </p>
    </div>
  `,
  styles: [`.child { border: 1px solid blue; padding: 10px; margin: 10px 0; }`]
})
export class ChildComponent {
  @Input() message: string = '';
}

When to use:

Child to Parent: @Output and EventEmitter

To send data from a child component to its parent, use the @Output decorator with an EventEmitter.

Child Component

// child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div class="child">
      <h3>Child Component</h3>
      <p>Message from parent: </p>
      <div>
        <label for="response-input">Response to parent: </label>
        <input id="response-input" [(ngModel)]="childMessage">
        <button (click)="sendMessage()">Send to Parent</button>
      </div>
    </div>
  `,
  styles: [`.child { border: 1px solid blue; padding: 10px; margin: 10px 0; }`]
})
export class ChildComponent {
  @Input() message: string = '';
  @Output() messageEvent = new EventEmitter<string>();
  
  childMessage = 'Message from child';
  
  sendMessage() {
    this.messageEvent.emit(this.childMessage);
  }
}

Parent Component

// parent.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <div>
      <label for="message-input">Message to child: </label>
      <input id="message-input" [(ngModel)]="parentMessage">
    </div>
    <p *ngIf="messageFromChild">Message from child: </p>
    <app-child 
      [message]="parentMessage"
      (messageEvent)="receiveMessage($event)">
    </app-child>
  `
})
export class ParentComponent {
  parentMessage = 'Message from parent';
  messageFromChild = '';
  
  receiveMessage(message: string) {
    this.messageFromChild = message;
  }
}

When to use:

Sharing Data with Services

For communication between unrelated components or components that are far apart in the component tree, Angular services provide an effective solution.

Shared Service

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private message = 'Default message';
  
  getMessage(): string {
    return this.message;
  }
  
  setMessage(message: string): void {
    this.message = message;
  }
}

First Component

// first.component.ts
import { Component } from '@angular/core';
import { DataService } from '../services/data.service';

@Component({
  selector: 'app-first',
  template: `
    <div class="component">
      <h3>First Component</h3>
      <div>
        <label for="message-input">Update shared message: </label>
        <input id="message-input" #messageInput>
        <button (click)="updateMessage(messageInput.value)">Update</button>
      </div>
      <p>Current message: </p>
    </div>
  `,
  styles: [`.component { border: 1px solid green; padding: 10px; margin: 10px 0; }`]
})
export class FirstComponent {
  message = '';
  
  constructor(private dataService: DataService) {
    this.message = this.dataService.getMessage();
  }
  
  updateMessage(message: string): void {
    this.dataService.setMessage(message);
    this.message = this.dataService.getMessage();
  }
}

Second Component

// second.component.ts
import { Component } from '@angular/core';
import { DataService } from '../services/data.service';

@Component({
  selector: 'app-second',
  template: `
    <div class="component">
      <h3>Second Component</h3>
      <p>Shared message: </p>
      <button (click)="refreshMessage()">Refresh Message</button>
    </div>
  `,
  styles: [`.component { border: 1px solid purple; padding: 10px; margin: 10px 0; }`]
})
export class SecondComponent {
  message = '';
  
  constructor(private dataService: DataService) {
    this.message = this.dataService.getMessage();
  }
  
  refreshMessage(): void {
    this.message = this.dataService.getMessage();
  }
}

When to use:

Using RxJS Subjects and BehaviorSubjects

For more reactive communication between components, RxJS Subjects and BehaviorSubjects provide a powerful solution.

Enhanced Data Service with BehaviorSubject

// data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private messageSource = new BehaviorSubject<string>('Default message');
  currentMessage = this.messageSource.asObservable();
  
  updateMessage(message: string): void {
    this.messageSource.next(message);
  }
}

First Component with RxJS

// first.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from '../services/data.service';

@Component({
  selector: 'app-first',
  template: `
    <div class="component">
      <h3>First Component</h3>
      <div>
        <label for="message-input">Update shared message: </label>
        <input id="message-input" #messageInput>
        <button (click)="updateMessage(messageInput.value)">Update</button>
      </div>
      <p>Current message: </p>
    </div>
  `,
  styles: [`.component { border: 1px solid green; padding: 10px; margin: 10px 0; }`]
})
export class FirstComponent implements OnInit {
  message = '';
  
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    this.dataService.currentMessage.subscribe(message => {
      this.message = message;
    });
  }
  
  updateMessage(message: string): void {
    this.dataService.updateMessage(message);
  }
}

Second Component with RxJS

// second.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from '../services/data.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-second',
  template: `
    <div class="component">
      <h3>Second Component</h3>
      <p>Shared message: </p>
    </div>
  `,
  styles: [`.component { border: 1px solid purple; padding: 10px; margin: 10px 0; }`]
})
export class SecondComponent implements OnInit, OnDestroy {
  message = '';
  private subscription: Subscription = new Subscription();
  
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    this.subscription = this.dataService.currentMessage.subscribe(message => {
      this.message = message;
    });
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

When to use:

Component Communication with NgRx

For complex applications with sophisticated state management needs, NgRx provides a comprehensive solution.

Actions

// message.actions.ts
import { createAction, props } from '@ngrx/store';

export const updateMessage = createAction(
  '[Message] Update',
  props<{ message: string }>()
);

Reducer

// message.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as MessageActions from './message.actions';

export interface MessageState {
  message: string;
}

export const initialState: MessageState = {
  message: 'Default message'
};

export const messageReducer = createReducer(
  initialState,
  on(MessageActions.updateMessage, (state, { message }) => ({
    ...state,
    message
  }))
);

Selectors

// message.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { MessageState } from './message.reducer';

export const selectMessageState = createFeatureSelector<MessageState>('message');

export const selectMessage = createSelector(
  selectMessageState,
  (state) => state.message
);

First Component with NgRx

// first.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { updateMessage } from '../store/message.actions';
import { selectMessage } from '../store/message.selectors';

@Component({
  selector: 'app-first',
  template: `
    <div class="component">
      <h3>First Component</h3>
      <div>
        <label for="message-input">Update shared message: </label>
        <input id="message-input" #messageInput>
        <button (click)="updateMessage(messageInput.value)">Update</button>
      </div>
      <p>Current message: </p>
    </div>
  `,
  styles: [`.component { border: 1px solid green; padding: 10px; margin: 10px 0; }`]
})
export class FirstComponent {
  message$: Observable<string>;
  
  constructor(private store: Store) {
    this.message$ = this.store.select(selectMessage);
  }
  
  updateMessage(message: string): void {
    this.store.dispatch(updateMessage({ message }));
  }
}

Second Component with NgRx

// second.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { selectMessage } from '../store/message.selectors';

@Component({
  selector: 'app-second',
  template: `
    <div class="component">
      <h3>Second Component</h3>
      <p>Shared message: </p>
    </div>
  `,
  styles: [`.component { border: 1px solid purple; padding: 10px; margin: 10px 0; }`]
})
export class SecondComponent {
  message$: Observable<string>;
  
  constructor(private store: Store) {
    this.message$ = this.store.select(selectMessage);
  }
}

When to use:

ViewChild and ContentChild

For direct access to child components, directives, or DOM elements, Angular provides the @ViewChild and @ContentChild decorators.

Parent Component with ViewChild

// parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <button (click)="callChildMethod()">Call Child Method</button>
    <app-child></app-child>
  `
})
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent) childComponent!: ChildComponent;
  
  ngAfterViewInit() {
    // Note: Be careful with changes in AfterViewInit to avoid ExpressionChangedAfterItHasBeenCheckedError
    setTimeout(() => {
      console.log('Child message:', this.childComponent.message);
    });
  }
  
  callChildMethod() {
    this.childComponent.showMessage('Called from parent');
  }
}

Child Component

// child.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div class="child">
      <h3>Child Component</h3>
      <p></p>
    </div>
  `,
  styles: [`.child { border: 1px solid blue; padding: 10px; margin: 10px 0; }`]
})
export class ChildComponent {
  message = 'Child component message';
  
  showMessage(msg: string) {
    alert(`Message: ${msg}`);
  }
}

When to use:

Component Interaction via Template Variables

For simpler scenarios, template variables provide a straightforward way to interact with child components.

Parent Component with Template Variable

// parent.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <app-child #childComp></app-child>
    <button (click)="childComp.showMessage('Called via template variable')">
      Call Child Method
    </button>
  `
})
export class ParentComponent {}

Child Component

// child.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div class="child">
      <h3>Child Component</h3>
      <p></p>
    </div>
  `,
  styles: [`.child { border: 1px solid blue; padding: 10px; margin: 10px 0; }`]
})
export class ChildComponent {
  message = 'Child component message';
  
  showMessage(msg: string) {
    alert(`Message: ${msg}`);
    this.message = msg;
  }
}

When to use:

Communication Between Sibling Components

Sibling components can communicate through a shared parent component or a shared service.

Parent Component as Mediator

// parent.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Component</h2>
    <app-sibling-a (messageEvent)="receiveMessage($event)"></app-sibling-a>
    <app-sibling-b [message]="siblingMessage"></app-sibling-b>
  `
})
export class ParentComponent {
  siblingMessage = '';
  
  receiveMessage(message: string) {
    this.siblingMessage = message;
  }
}

Sibling A Component

// sibling-a.component.ts
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-sibling-a',
  template: `
    <div class="sibling">
      <h3>Sibling A Component</h3>
      <div>
        <label for="message-input">Message to Sibling B: </label>
        <input id="message-input" [(ngModel)]="message">
        <button (click)="sendMessage()">Send</button>
      </div>
    </div>
  `,
  styles: [`.sibling { border: 1px solid orange; padding: 10px; margin: 10px 0; }`]
})
export class SiblingAComponent {
  @Output() messageEvent = new EventEmitter<string>();
  message = 'Message from Sibling A';
  
  sendMessage() {
    this.messageEvent.emit(this.message);
  }
}

Sibling B Component

// sibling-b.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-sibling-b',
  template: `
    <div class="sibling">
      <h3>Sibling B Component</h3>
      <p *ngIf="message">Message from Sibling A: </p>
      <p *ngIf="!message">No message received yet</p>
    </div>
  `,
  styles: [`.sibling { border: 1px solid teal; padding: 10px; margin: 10px 0; }`]
})
export class SiblingBComponent {
  @Input() message = '';
}

When to use:

Best Practices

  1. Choose the Right Pattern: Select the communication pattern based on the relationship between components and the complexity of your application.

  2. Unsubscribe from Observables: Always unsubscribe from Observables in the ngOnDestroy lifecycle hook to prevent memory leaks.

    export class MyComponent implements OnInit, OnDestroy {
      private subscription = new Subscription();
         
      ngOnInit() {
        this.subscription = this.dataService.data$.subscribe(/* ... */);
      }
         
      ngOnDestroy() {
        this.subscription.unsubscribe();
      }
    }
    
  3. Use OnPush Change Detection: For better performance, use OnPush change detection strategy when possible.

    @Component({
      selector: 'app-my-component',
      template: `...`,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class MyComponent { /* ... */ }
    
  4. Keep Components Focused: Each component should have a single responsibility. Avoid creating components that try to do too much.

  5. Use Immutable Data: When passing data between components, use immutable patterns to prevent unexpected side effects.

  6. Document Component Interfaces: Clearly document the inputs and outputs of your components to make them easier to use.

  7. Avoid Excessive Nesting: Deep component hierarchies can make communication more complex. Consider flattening your component structure when possible.

Common Pitfalls and Solutions

  1. Circular Dependencies: When services depend on each other, it can create circular dependencies. Solve this by using forwardRef or restructuring your services.

  2. Memory Leaks: Forgetting to unsubscribe from Observables can cause memory leaks. Use the async pipe, takeUntil, or manually unsubscribe in ngOnDestroy.

  3. ExpressionChangedAfterItHasBeenCheckedError: This error occurs when a value changes after change detection. Solve it by using ngZone.run() or setTimeout().

  4. Overusing @Input and @Output: Too many inputs and outputs can make components hard to use. Consider using a service or a more structured approach for complex communication.

  5. Prop Drilling: Passing data through multiple levels of components can be cumbersome. Use services or state management for deeply nested components.

Conclusion

Effective component communication is essential for building maintainable Angular applications. By understanding the various patterns available and choosing the right one for your specific use case, you can create a clean, efficient architecture that scales with your application.

For simple parent-child communication, @Input and @Output decorators are usually sufficient. For more complex scenarios, services with RxJS Subjects provide a flexible solution. And for large applications with complex state management needs, NgRx offers a comprehensive framework.

Remember to follow best practices, such as unsubscribing from Observables and using immutable data patterns, to avoid common pitfalls and ensure your application performs well.

References