Hey Guys!

Every developer wants to build fast and robust Angular applications. But to be able to, you need to dive into the Angular internal structure and obtain comprehensive understanding how it works and sometimes it is not that easy from the first try.

In this article, I'll try to give you a superficial idea of how Angular works under the hood not going in too much of detail.

The main concept of the Angular internals that we need to discuss is Change Detection - process that checks if data has been changed and re-renders components’ templates.

Let's break it into the following topics:

  • The internal representation of Angular applications
  • (Who triggers change detection)
  • What change detection does
  • How Angular updates DOM
  • Why we need ExpressionChangedAfterItHasBeenCheckedError

Internal representation

When you build an Angular application you start from AppComponent, then you create more and more components, combine them in modules and use these components here and there. Thus, we can imagine Angular application as a tree of components, but it only scratches the surface. Let's figure out what the Angular application actually consists of.

If you create a simple component like this:

and build your application in aot mode, you'll be able to find component factory generated by the Angular compiler. It will look like a simplified factory:

Component factory describes the structure of the component view and is used when instantiating ViewDefinition.

ViewDefinition is like a template which is used when instantiating components. For each component, Angular instantiates ViewData that contains component instance, its state, children, parent and bounded properties.

As we can see now, the Angular application is not just a tree of components but a tree of ViewData instances and our components are just fields of that view data instances. Moreover, Angular runs change detection process over ViewData, not over components itself.

Who triggers change detection?

Ok, now we know how angular represents our applications and what instances are used to perform change detection’s tasks.

Before digging into change detection process I want to tell you what actions and events are responsible for triggering this process:

  • Developer through ChangeDetectorRef
  • Output events of the components
  • Async browser APIs (XHR, setTimeout, click, etc.) with help of Zone.js.

More details you can find by the links above but let's focus on Zone.js now.

Zone.js

I don't want to be too specific about Zone but in few words, you can think about it as a tool that provides you with a capability to hook into your code execution process and track when a part of your code has started or finished execution. For example in the case below we use afterTask hook that will be run when a code in the run method finished its execution, including asynchronous tasks. So this hook will be executed only when setTimeout is completed:

And that’s a point. Zone is an execution context that persists across async tasks. Angular has Zone wrapper called NgZone that gives a capability to do something similar to the code below, meaning, subscribe to events fired by zone and run change detection loop:

Can I run something outside change detection?

Angular subscribes to the most of the browser events and runs change detection as a reaction but if you want to run some process that doesn't require DOM updates you can run it outside change detection within runOutsideAngular method of the NgZone. This may be useful for sending analytics to the server from time to time because we don't need to update application UI during this process.

Change detection flow

It's time to investigate what change detection exactly does. If it’s been triggered it starts from the root component, checks it and proceeds to the children. As you can see in the picture below change detection can be triggered on any component and always starts from the application root.

Change detection flow

During the change detection on a component angular does the following steps:

  • Update input properties for child components
  • Call ngOnChanges, ngOnInit and ngDoCheck hooks on all child components
  • Update DOM for the current component
  • Run change detection for all child components
  • Call ngAfterViewInit lifecycle hook for all child components

Let's imagine that we have three components:

Change Detection flow for these components will be the following:

  • Checking A component:
  • update B input bindings
  • call ngOnChanges, ngOnInit and ngDoCheck on the B component
  • update DOM for component A
  • Checking B component:
  • update C input bindings
  • call ngOnChanges, ngOnInit and ngDoCheck on the C component
  • update DOM for component B
  • Checking C component:
  • update DOM for component C

OnPush Strategy

As we can see, calling ngOnChanges and ngDoCheck hooks doesn't mean component has been checked. These hooks are called when change detection process is focused on the component parent, so ngDoCheck of the B component will be called when change detection is checking A component.
This may lead your application to the strange behavior when you're using OnPush strategy.

I mean, if we make changeDetectionStrategy of B component OnPush and don't change input props B component will not be checked but ngDoCheck hook will be triggered.

  • Checking A component:
  • update B input bindings
  • call ngDoCheck on the B component
  • update DOM interpolations for component A
  • if (bindings changed) -> Checking B component

OnPush Strategy Emulation

For a better understanding how OnPush strategy works, let's see how we can emulate it. Using ChangeDetectorRef of the component we can detach it when instantiating and stop detecting changes on it. Anyway, as we know from examples above ngOnChanges hook will be called on this component even though it's detached because it'll be called when change detection checks its parent.

ngOnChanges will be called only if property binding has been changed. Sounds similar to the OnPush strategy, right? So, if input bindings have changed we have to reattach component and allow change detection process it. And then detach it again asynchronously.

DOM updates

I think it's time to discuss the most important step of change detection - updating DOM representation of the checked component.

Do you remember this simple component from the first part of the article?

Let's look closer to another part of the component factory that wasn't mentioned previously:

The purpose of this function is to check and update component DOM representation if bindings are changed.

Update renderer function receives checkAndUpdate function and ViewData instance that contains current component. It calls checkAndUpdate for each binding in the component template. If you have multiple bindings in template like these:

Update renderer will perform multiple calls of the checkAndUpdate:

checkAndUpdate function is just a router that chooses specific checkAndUpdate* function depends on binding type:

I think the simplest checkAndUpdate function is checkAndUpdateText and its code looks like the snippet below. It just checks that rendered values and new ones that aren't the same and then updates component DOM:

This means that each property of component template will be checked and updated separately, and if you changed only one text node of the component only this text node will be re-rendered, not all the template. This behavior helps angular reach high-speed rendering.

Why we need ExpressionChangedAfterItHasBeenChecked Error?

Angular has another important underlying concept that helps us to keep view and model of an application consistent. Let's look at this example:

According to the change detection steps for this example:

  • Update DOM for the current component
  • Call ngAfterViewInit lifecycle hook for all child components

ngAfterViewInit of the ChildComponent will be called after updating the DOM of the ParentComponent. However, ngAfterViewInit changes parents name property which rendered in the parent template. Do you see that? Something goes wrong... Change detection has rerendered ParentComponent and then its child changes its name property.

And after that, we have inconsistency - rendered name property of the ParentComponent isn't equal actual name property because it has been changed after rendering.

Great, we’ve defined the problem, so has Angular. This is the reason why angular runs second change detection loop in the development mode for verification purpose. This verification loop checks that actual properties in components equal rendered and bounded properties and if they do not, then angular throws ExpressionChangedAfterItHasBeenCheckedError.

Let's check another example which reveals property binding issue:

We have a quite similar example to the previous one but with two differences:

1) ParentComponent passes input property for its ChildComponent
2) ChildComponent changes ParentComponent.text property in its ngOnInit hook

This code leads us to the similar error as mentioned above, but why?

It changes input bindings between parent and itself. As you remember from Change Detection Flow part, the order of change detection operations is the following for this example:

  • Update input properties for child components
  • Call ngOnChanges, ngOnInit and ngDoCheck hooks on all child components

But in the snippet, we change parent input binding in ngOnInit hook - after binding has been updated. Verification loop checks this inconsistency and throws the exception.

If you have this error it means you're doing something wrong with updating component properties when it had already been rendered or bounded.

To avoid such error, you have to remember about change detection operations order and update your data in the right lifecycle hooks.

Hopefully, you've found this topic useful. Keep reading our blog and see you on our meet-ups!

About Akveo

Akveo is an experienced team of full-stack software experts passionate about creating reliable software and ready to accept the next tech challenge. Our expertise lets us understand the essence of our clients' business needs to deliver the best solution possible. Plus, the use of our own products in development and design allows us to reduce development time and implement new solutions faster. Check what our customers say and contact us.

Loading Comments