Angular has two types of change detection. As a brief review, those two types are the Default
strategy and the OnPush
strategy.
The Default
strategy runs every time any change happens in the app. It could be a button click, an HTTP call, a setTimeout
, or any other type of timer or user interaction.
The OnPush
strategy on the other hand, only runs if one of four conditions are met:
- The
Input
reference changes - An event originated from the component or one of its children (an
@Output
event fires) - Change detection is explicitly run
- The
async
pipe receives new data
In this article, we'll talk mainly about how to make sure the UI updates properly when case 1 or 2 occurs. If you'd like more details on Angular's change detection, go read this article by Netanel Basal.
Now, I came across this topic while watching Dan Wahlin's latest Pluralsight course on Angular Architecture. He talked about the benefits of using the OnPush
change detection strategy, but that when doing so there may be times where your UI doesn't update. Here's a situation where your application may not update automatically, even though you are using the @Input
and @Output
methods in the list above that should trigger change detection.
Let's pretend you have an application where a user can add or edit their favorite books. Each book has a title, an author, a rating, and an ID. When the application loads, it calls a service to get the list of books, and then caches them in the service. After that, when the books are retrieved they are just retrieved from the service. The user can select a book to edit or can add a new book. When that happens, the book is added directly to the cached list of books, or updated in place. As for the components, you have a parent component which contains a child component that lists the books and another child component that allows for the editing of those books. Those two child components are the ones with the OnPush
change detection set. Below you can see the interface and the service as an example.
export interface Book {
id: number;
author: string;
title: string;
rating: number;
}
@Injectable({ providedIn: 'root' })
export class BooksService {
private books: Book[] = [{
id: 1,
author: 'Brandon Mull',
title: 'Fablehaven',
rating: 3
}];
constructor() { }
getBooks(): Observable {
return of(this.books);
}
updateBook(book: Book): Observable {
const idx = this.books.findIndex((bk: Book) => bk.id === book.id);
this.books[idx] = book;
return of(this.books);
}
addBook(): Observable {
console.log('called')
this.books.push({
id: this.books.length + 1,
title: 'New Book',
author: 'Some Author',
rating: 3
});
return of(this.books);
}
}
Now, in this situation, when a book is added or edited and the change made to the cached list of books, the UI for the application won't update. This is because the inputs to the two children components are either objects (for the selected book to edit) or an array (to display all the books). Because they are objects/arrays, any change made to the contents will not trigger a change detection cycle. That's because they are still pointing to the same location in memory; in other words the inputs are passed by reference. Technically, according to Angular, the variable never changed so change detection doesn't run.
We can change this by the way we return the list of objects each time one is added or edited. Instead of returning the same array each time, we can return a cloned array. This changes its location in memory each time the list is updated and the UI is updated as a result. Here's an example of the service written that way:
@Injectable({ providedIn: 'root' })
export class Books2Service {
private books: Book[] = [{
id: 1,
author: 'Brandon Mull',
title: 'Fablehaven',
rating: 3
}];
constructor() { }
private cloneBooksArray() {
return JSON.parse(JSON.stringify(this.books));
}
getBooks(): Observable {
return of(this.cloneBooksArray());
}
updateBook(book: Book): Observable {
const idx = this.books.findIndex((bk: Book) => bk.id === book.id);
this.books[idx] = book;
return of(this.cloneBooksArray());
}
addBook(): Observable {
this.books.push({
id: this.books.length + 1,
title: 'New Book',
author: 'Some Author',
rating: 3
});
return of(this.cloneBooksArray());
}
}
Now, I know this is a little tough to understand in writing only, so I prepared a StackBlitz example to demonstrate. All you need to do is to change the service that is used in the app.component.ts
file. The BooksService
doesn't clone the books array that is returned; the Books2Service
does clone the array. Thus, the second service updates the UI while the first service doesn't.
In this example, since it's a pretty basic one, I used JSON.stringify()
and JSON.parse()
to clone the array each time. If the objects or arrays are more complex, or have date properties, you may want to reach for a better solution. There are many third-party libraries out there that will clone objects for you, and any one of them would work.
Top comments (0)