Published on July 15, 2023
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.
Before diving into specific techniques, it’s important to understand the different types of component relationships in Angular:
The communication technique you choose should be based on the relationship between the components and the specific requirements of your application.
The simplest form of component communication is passing data from a parent component to a child component using the @Input
decorator.
// 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.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:
To send data from a child component to its parent, use the @Output
decorator with an EventEmitter
.
// 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.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:
For communication between unrelated components or components that are far apart in the component tree, Angular services provide an effective solution.
// 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.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.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:
For more reactive communication between components, RxJS Subjects and BehaviorSubjects provide a powerful solution.
// 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.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.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:
For complex applications with sophisticated state management needs, NgRx provides a comprehensive solution.
// message.actions.ts
import { createAction, props } from '@ngrx/store';
export const updateMessage = createAction(
'[Message] Update',
props<{ message: string }>()
);
// 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
}))
);
// 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.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.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:
For direct access to child components, directives, or DOM elements, Angular provides the @ViewChild
and @ContentChild
decorators.
// 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.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:
For simpler scenarios, template variables provide a straightforward way to interact with child components.
// 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.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:
Sibling components can communicate through a shared parent component or a shared service.
// 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.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.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:
Choose the Right Pattern: Select the communication pattern based on the relationship between components and the complexity of your application.
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();
}
}
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 { /* ... */ }
Keep Components Focused: Each component should have a single responsibility. Avoid creating components that try to do too much.
Use Immutable Data: When passing data between components, use immutable patterns to prevent unexpected side effects.
Document Component Interfaces: Clearly document the inputs and outputs of your components to make them easier to use.
Avoid Excessive Nesting: Deep component hierarchies can make communication more complex. Consider flattening your component structure when possible.
Circular Dependencies: When services depend on each other, it can create circular dependencies. Solve this by using forwardRef or restructuring your services.
Memory Leaks: Forgetting to unsubscribe from Observables can cause memory leaks. Use the async pipe, takeUntil, or manually unsubscribe in ngOnDestroy.
ExpressionChangedAfterItHasBeenCheckedError: This error occurs when a value changes after change detection. Solve it by using ngZone.run() or setTimeout().
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.
Prop Drilling: Passing data through multiple levels of components can be cumbersome. Use services or state management for deeply nested components.
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.