Angualr Change Detection is always a fasinating topic, there are tons and tons articals and videos about it in the internet. Every now and then, a new one will will come up to talk about it. After watching the video about Angular : A Design Review 10 Years Later released by ng-conf a few month ago, I want to review how my understanding changes the way I program with Angualr.
This is the first post of a series, however, I will start from the end, not the beginning. In the end, what matters is what we produce, but not how we understand it. In this post, I will implement a simple app firstly with normal pratice, then with the “Best Practice”. I will explain why these are best practice in the future post, because it takes much more time.
The features of the sample app
- A button that can increment a number.
- A running clock.
- When initialized, the app fetches a list of users from github api and populates them to a dropwdown list
- A dropdown to switch user profile.
- A image to show user’s avatar.
Here is the starting point. You can navigate to different files in the file explorer.
View state can live anyware, no wrappers, no getters or setters, and no api need to call, no $digest, $apply, it just works
— Alex Rickabaugh (a member of Angualr team)
Angular team assume user can use whatever data structure to represent their view model, and place them wherever they want. Ok, let’s follow some “Best practice” and do some refactory.
Best practice #1 Use OnPush Component everywhere.
Simple, just add one line
changeDetection: ChangeDetectionStrategy.OnPush to
@Component meta data like the following
After this little tiny change, the app is broken, the clock is no longer updated, the lookup data is not populated into the dropdown. However, the other increment button works fine, but clicking it will also update the clock time and populate lookup data to the dropdown list, which are unintended side effects. What just happened? People normally panic after so many things got broken, and quickly revert it back to default change detection strategy.
Let’s solve this problem with best practice #2.
Best practice #2. Use observable everywhere
There are the changes
- convert the time from date type to an observable
- instead of subscribing the observable programatically, subscribe it using async pipe declaratively.
Best practice #3. Separate big component into smaller ones
Right now, all the feature is implemented in the same component. Let’s follow this best practice and break it down to dumb component. Here fore more component are created to implment the features of the root component. All user components are surrounded with a orange border.
- a count component (dump component)
- a clock component (smart component)
- a user switcher component (dump component)
- a user info display component (dump component)
Best practice #4: “Micro” Optimization
Right now, the everything works, but there is a little annoying thing. The
clock component trigger a change detection every second. For a small app, that
is ok. If it is put into a large tree of components, the scope of change detection
may be bigger than you think, it could affect the performance of the tree.
ngZone.runOutsideAngular to rescure.
After the change, the
setInterval call will not trigger the change detection
from the root component every second, and the scope of change detection is
limited to within the clock only.
If a component’s state is effected purely from input parent, and nothing else, and it does not cause side effect other than popup events to parent, it is called pure component, dump component, or presentation component. Examples are the count component, user switch component, user info display component. The OnPush change detection strategy works flawlessly with this kind of component.
Otherwise, it is smart component. For example, the root component is a small
component, because it gets lookup data from network. The clock component
is also a smart component, because it has changing time state internally. If we
use OnPush change detection strategy for it, it can be broken, just as we see
in the step 2. But it doesn’t mean that we can’t use it in smart component, the
solution is to use observable instead of primitive JavaScrpt data type and
async pipe instead of manual subscription.
It may seem easy to follow these “best practices”, but in reality, using observable everywhere can be quite challenging. However, it is achievable with effort. To understand why they are best practice, we need to have an understanding of how Angular change detection operates. I will delve into this topic further in my upcoming posts.