@Input() animal = "Tiger"
editAnimal(animal: string){
this.animal = animal
}
The editAnimal function runs when a user types a new animal into a text field and presses a button to submit. Everything works as expected, but the ngOnChanges is not running, but ngDoCheck is. Can someone explain why?
There's not a lot of context about the parent component and the flow of the data to the child component on the given example.
ngOnChanges()
and ngDoCheck()
However, as mentioned on the official Angular Documentation - Lifecycle hooks:
ngOnChanges()
Called before ngOnInit() (if the component has bound inputs) and whenever one or more data-bound input properties change.
Assuming that in your example, there's a parent component that passes the animal
data down to the child component, the data will be updated once before ngOnInit()
is called. If the input data are not changing, this is the reason why you're not seeing the ngOnChanges()
firing again - otherwise you would see the ngOnChanges()
being executed.
Now coming to ngDoCheck()
. Again, as stated to the official documentation:
ngDoCheck()
Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run.
The ngDoCheck()
is called after every change detection cycle run - which makes it also a very expensive hook to implement (can have a very severe penalty on performance). This lifecycle hook is executed whenever a change has occured, no matter where the change occured on the component's tree.
To make it clearer, we can consider the following example. Imagine that we have an application that displays some events on the screen, and users can click on events to see more details. For the sake of our example, we have only 2 events in our list - the Google Demo Conference and Angular Demo Conference.
Top Level Component: The top level component has 2 child components, a Navigation Bar Component and an Events List Component. The Navigation Bar Component contains also a search box, where users can type in and upon hitting enter, see relevant events.
Events List Component: The Events List Component has only one child component - the EventsListItemComponent
.
Imagine a rough implementation of the EventsListItemComponent
as follows (notice below that we have implemented the ngDoCheck()
and ngOnChanges()
lifecycle hooks):
@Component({
selector: "events-list-item",
templateUrl: "events-list-item.component.html",
styleUrls: ["events-list-item.component.css"]
})
export class EventsListItemComponent {
@Input() public event!: IEvent;
@Output() public eventClickEmitter = new EventEmitter();
public ACTION_BUTTON_TEXT: string = "Book Now";
constructor(private toastrService: ToastrServiceWrapper,
@Inject(EVENT_LIST_ACTION_BUTTON_TEXT) actionButtonText: string) {
this.ACTION_BUTTON_TEXT = actionButtonText;
}
public eventListItemClicked(eventId: number): void {
console.log("Event Item #ID: " + eventId + " clicked");
this.eventClickEmitter.emit(eventId);
}
ngOnChanges() {
console.log("ngOnChanges() " + this.event.title);
}
ngDoCheck() {
console.log("ngDoCheck() " + this.event.title);
}
public log(text: string): void {
console.log(text);
}
}
The Navigation Bar Component looks like the below:
<div class="toolbar" role="banner">
<img width="40" alt="Angular Logo"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==" />
<span>Welcome</span>
<div class="spacer"></div>
<a class="nav-link" aria-label="Search Event" title="Search Events">
<form #searchEventsForm="ngForm" (ngSubmit)="searchEvent(searchTerm)">
<div>
<input [(ngModel)]="searchTerm" name="searchTerm" autocomplete="off" novalidate>
</div>
</form>
</a>
<a class="nav-link" [routerLink]="['/events']" routerLinkActive="nav-link-active"
[routerLinkActiveOptions]="{exact: true}" aria-label="Events List" title="Events List">
Events
</a>
import { Component } from "@angular/core";
import { AuthService } from "../modules/shared/auth.service";
import { EventsService, IEvent } from "../events";
@Component({
selector: "nav",
templateUrl: "nav.component.html",
styleUrls: ["nav.component.css"]
})
export class NavComponent {
public searchTerm: string = "";
constructor(private authService: AuthService,
private eventsService: EventsService) { }
public isUserAuthenticated(): boolean {
return this.authService.isAuthenticated();
}
public fetchUserNameOfAuthenticatedUser(): string {
let userNameOfAuthenticatedUser: string = "";
if (this.authService.isAuthenticated()) {
userNameOfAuthenticatedUser = this.authService.currentUser.firstName;
}
return userNameOfAuthenticatedUser;
}
public searchEvent(searchTerm: string) {
let matchedEvents: IEvent[] = this.eventsService.searchEvent(searchTerm);
console.log("Matched events for search term: " + searchTerm);
console.log(matchedEvents);
}
}
When the component initially loads, the following will be logged in the console:
ngOnChanges() Angular Demo Conference events-list-item.component.ts:32
ngDoCheck() Angular Demo Conference events-list-item.component.ts:28
ngOnChanges() Google Demo Conference events-list-item.component.ts:32
ngDoCheck() Google Demo Conference
Consider now the situation where a user just types into the navigation bar component's search box. This will trigger the ngDoCheck()
of the EventsListItemComponent
to be re-executed:
Depending on the number of letters typed, it would look something like the below:
ngOnChanges() Angular Demo Conference events-list-item.component.ts:32
ngDoCheck() Angular Demo Conference events-list-item.component.ts:28
ngOnChanges() Google Demo Conference events-list-item.component.ts:32
ngDoCheck() Google Demo Conference new entries events-list-item.component.ts:32 ngDoCheck() Angular Demo Conference
events-list-item.component.ts:32 ngDoCheck() Google Demo Conference
events-list-item.component.ts:32 ngDoCheck() Angular Demo Conference
events-list-item.component.ts:32 ngDoCheck() Google Demo Conference
events-list-item.component.ts:32 ngDoCheck() Angular Demo Conference
events-list-item.component.ts:32 ngDoCheck() Google Demo Conference
events-list-item.component.ts:32 ngDoCheck() Angular Demo Conference
events-list-item.component.ts:32 ngDoCheck() Google Demo Conference
A visual representation of what was described above:
Notice on the NavigationBarComponent
the bi-directional binding of the searchTerm
property between the template and the component directive, i.e., [(ngModel)]="searchTerm"
. So, every time we type a letter into the searchboxe's <input>
property, the relevant binding is updating.
Angular is detecting the changes on the Data Model of the NavigationBarComponent
which triggers a traversal to the hierarchical tree-component structure.
At runtime, Angular creates a separate change detector class for every component in the tree, which then eventually forms a hierarchy of change detectors similar to the hierarchy tree of components.
Whenever change detection is triggered, Angular walks down this tree of change detectors to determine if any of them have reported changes.
This is when the ngDoCheck()
is called in the EventsListItemComponent
- in order for changes that Angular can't catch by it's own to be detected. I suppose that something similar happens in your application as well.
We could go on and talk about detection strategies and more about Angular, however, this could make the post even more bigger.
Some good readings and sources that have been used for this post: