Fundamentals: Parent-Child Communication
Purpose: Allows parent components to pass data to child components.
Usage: Decorate a property in the child component with @Input() .
|
// Child Component
import { Input } from '@angular/core';
export class ChildComponent {
@Input() message: string;
}
<!-- Parent Template -->
<app-child [message]="parentMessage"></app-child>
|
Best Practices:
- Use descriptive input names.
- Consider using
OnChanges lifecycle hook to react to input changes.
|
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
export class ChildComponent implements OnChanges {
@Input() message: string;
ngOnChanges(changes: SimpleChanges) {
if (changes['message']) {
console.log('Message changed:', changes['message'].currentValue);
}
}
}
|
Common Mistakes:
- Forgetting to import
Input .
- Not initializing input properties.
|
|
Purpose: Allows child components to emit events that parent components can listen to.
Usage: Decorate an EventEmitter property with @Output() .
|
// Child Component
import { Output, EventEmitter } from '@angular/core';
export class ChildComponent {
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('Hello from child!');
}
}
<!-- Parent Template -->
<app-child (messageEvent)="receiveMessage($event)"></app-child>
|
Best Practices:
- Use specific event names related to the data being emitted.
- Always emit after a relevant action occurs.
|
// Parent Component
receiveMessage(message: string) {
this.receivedMessage = message;
}
|
Common Mistakes:
- Forgetting to subscribe to the event in the parent template.
- Not instantiating the
EventEmitter .
|
|
Purpose: Allows data to flow both ways between parent and child components.
Usage: Combine @Input() and @Output() with the naming convention propertyNameChange .
|
// Child Component
import { Input, Output, EventEmitter } from '@angular/core';
export class ChildComponent {
@Input() value: string;
@Output() valueChange = new EventEmitter<string>();
onValueChange(newValue: string) {
this.value = newValue;
this.valueChange.emit(newValue);
}
}
<!-- Parent Template -->
<app-child [(value)]="parentValue"></app-child>
|
Best Practices:
- Use for simple data synchronization.
- Consider alternative approaches for complex scenarios.
|
|
Common Mistakes:
- Incorrect naming convention for the output event (
propertyNameChange ).
- Creating infinite loops by emitting changes without proper checks.
|
|
Component Access and Sibling Communication
@ViewChild: Access a child component directly within the parent component’s template.
@ContentChild: Access a component projected into the current component using content projection (<ng-content> ).
|
// Parent Component
import { ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';
export class ParentComponent implements AfterViewInit {
@ViewChild(ChildComponent) child: ChildComponent;
ngAfterViewInit() {
console.log(this.child.message); // Access child property
}
}
|
Best Practices:
- Use
AfterViewInit or AfterContentInit lifecycle hooks to access child components after they’ve been initialized.
- Be mindful of potential
undefined values before initialization.
|
<!-- Parent Template -->
<app-child></app-child>
|
Common Mistakes:
- Accessing
ViewChild or ContentChild before the view or content is initialized.
- Not handling the case where the child component might not exist.
|
|
Purpose: Enable communication between components that don’t have a direct parent-child relationship.
Usage: Create a shared service with an RxJS Subject or BehaviorSubject.
|
// Shared Service
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
private messageSource = new Subject<string>();
message$ = this.messageSource.asObservable();
sendMessage(message: string) {
this.messageSource.next(message);
}
}
|
Best Practices:
- Use
BehaviorSubject to provide an initial value to new subscribers.
- Unsubscribe from the observable to prevent memory leaks (
takeUntil or AsyncPipe ).
|
// Component 1
import { DataService } from './data.service';
constructor(private dataService: DataService) {}
sendMessage() {
this.dataService.sendMessage('Hello from Component 1!');
}
// Component 2
import { DataService } from './data.service';
import { Subscription } from 'rxjs';
message: string;
subscription: Subscription;
constructor(private dataService: DataService) {
this.subscription = this.dataService.message$.subscribe(message => this.message = message);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
|
Common Mistakes:
- Forgetting to unsubscribe from the Subject, leading to memory leaks.
- Not injecting the service in the
providedIn: 'root' or in the module.
|
|
Advanced Communication Scenarios
Communication: Use a shared service at the application level (provided in root ) or a state management solution like NgRx, since components in lazy-loaded modules have their own injectors.
Example: Ensure a service is provided in the root module to make it accessible across all modules, including lazy-loaded ones.
|
// app.module.ts
@NgModule({
providers: [DataService] // Provided in root
})
export class AppModule { }
|
Purpose: Transferring data between components navigated through routing.
Methods:
- Route Parameters: For simple IDs or data.
- Query Parameters: Non-essential, optional data.
- Shared Service: More complex data or frequent communication.
- State Management (NgRx): For maintaining application state.
|
// Route Configuration
{ path: 'details/:id', component: DetailsComponent }
|
Best Practices:
- Use route parameters for mandatory data.
- Use a shared service for sharing state across routes, ensuring proper lifecycle management.
|
// Component accessing route parameter
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
this.id = +params['id']; // (+) converts string 'id' to a number
});
}
|
Techniques:
- takeUntil: Completes an observable when another observable emits.
- AsyncPipe: Automatically subscribes and unsubscribes from observables in templates.
|
// takeUntil example
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.message$.pipe(
takeUntil(this.destroy$)
).subscribe(message => this.message = message);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
|
Best Practices:
- Always unsubscribe from observables, especially in components that are frequently created and destroyed.
- Use
takeUntil or AsyncPipe for automatic unsubscription.
|
<!-- AsyncPipe example -->
<div>{{ message$ | async }}</div>
|
State Management and Strategy Comparison
Overview: NgRx is a reactive state management library inspired by Redux, suitable for large-scale applications requiring predictable state management.
Key Components:
- State: Single source of truth for the application state.
- Actions: Events that trigger state changes.
- Reducers: Pure functions that update the state based on actions.
- Selectors: Functions that extract data from the state.
|
When to Use:
- Complex applications with many components and shared state.
- Applications requiring predictable and testable state management.
- Applications benefiting from time-travel debugging.
|
Strategy |
Use Case |
Complexity |
Data Flow |
Lifecycle |
Memory Management |
@Input() |
Parent to child data transfer |
Low |
Parent -> Child |
Component lifecycle |
N/A |
@Output() |
Child to parent event emission |
Low |
Child -> Parent |
Component lifecycle |
N/A |
[()] |
Two-way data binding |
Low to Medium |
Parent <-> Child |
Component lifecycle |
N/A |
@ViewChild / @ContentChild |
Direct component access |
Medium |
Direct access |
AfterViewInit / AfterContentInit |
N/A |
Shared Service (RxJS) |
Sibling or cross-hierarchy communication |
Medium |
Any -> Any |
Service lifecycle |
Requires unsubscription |
State Management (NgRx) |
Application-wide state management |
High |
Centralized |
Application lifecycle |
Managed by NgRx |
|