Change detection and component trees in Angular applications

Post Editor

We can think of an Angular application as a tree of components. Under the hood, for components Angular uses a low-level abstraction called View. In this article we'll take a detailed look how component tree corresponds to a tree of views

5 min read
0 comments
post

Change detection and component trees in Angular applications

We can think of an Angular application as a tree of components. Under the hood, for components Angular uses a low-level abstraction called View. In this article we'll take a detailed look how component tree corresponds to a tree of views

post
post
5 min read
0 comments
0 comments
This article is an excerpt from my Angular Deep Dive course

With component based approach in web applications, composition is achieved by including child components in templates. For this reason, we can think of an Angular application as a tree of components. However, under the hood, for components Angular uses a low-level abstraction called View. It’s a smallest grouping of elements which are created and destroyed together. All operations like property checks and DOM updates are performed on views, hence it’s more technically correct to state that angular is a tree of views, while a component can be described as a higher level concept of a view.

The structure of a View is defined by the LView interface. LView stores all of the information needed to process the instructions as they are invoked from the template. Each component and an embedded view has its corresponding LView. We can refer to views created for components as component views to distinguish from the embedded views created through ViewContainerRef using template references, i.e.ng-template elements.

The hierarchy of views is tracked using designated properties on LView:

<>Copy
export const PARENT = 3; export const NEXT = 4; export const CHILD_HEAD = 13; export const CHILD_TAIL = 14; export interface LView { [CHILD_HEAD]: LView|LContainer|null; [CHILD_TAIL]: LView|LContainer|null; [PARENT]: LView|LContainer|null; [NEXT]: LView|LContainer|null; }

To traverse the tree of views Angular uses these traversal utils.

Angular also implements TView data structure that holds static data for an LView. This TView is shared between all LViews of a given type. This means that each instance of a particular component has its own instance of LView, but they all reference the same instance of TView.

The last bit we need to know is that Angular has a few different view types defined like this:

<>Copy
export const enum TViewType { Root = 0, Component = 1, Embedded = 2, }

Component and Embedded view types should be self explanatory. The Root view is a special type of views that Angular uses to bootstrap top-level components into. It is used in conjunction with LView which takes an existing DOM node not owned by Angular and wraps it in LView so that other components can be loaded into it.

There is a direct relationship between a view and a component — one view is associated with one component and vice verse. A view holds a reference to the associated component class instance in the CONTEXT property. All operations like property checks and DOM updates are performed on views.

For a template that includes A component twice the data structures will look like this:

Content imageContent image

Change detection tree
Link to this section

In most applications you have one main tree of component views that starts with the component you reference in the index.html. There are other root views that are created through portals, mostly for modal dialogs, tooltips etc. These are the UI elements that need to be rendered outside of the hierarchy of the main tree mostly for styling purposes, for example, so that they are not effected by overflow:hidden.

Angular keeps top level elements of such trees in a _views property of the ApplicationRef. Those trees are referred to as change detection trees, because they are traversed when Angular runs change detection globally. The tick method that runs change detection iterates over each tree in _views and runs the check for each view by calling detectChanges method:

<>Copy
@Injectable({ providedIn: 'root' }) export class ApplicationRef { tick(): void { try { this._runningTick = true; for (let view of this._views) { view.detectChanges(); } if (typeof ngDevMode === 'undefined' || ngDevMode) { for (let view of this._views) { view.checkNoChanges(); } } } catch (e) { ... } finally { ... } } }

You can also see that tick runs checkNoChanges method on the same set of views.

Attaching dynamic views to ApplicationRef
Link to this section

Angular allows to render a component into a standalone DOM element outside of Angular’s change detection tree. But since those views also need to be checked, ApplicationRef implements methods attachView() and detachView() to add/remove standalone views to change detection trees. This effectively adds those views to the _views array that’s traversed during change detection.

Let’s see this example. We have M component that we want to instantiate dynamically and then render into the DOM that’s outside main Angular tree. Here’s how we do it:

<>Copy
@Component({ selector: 'l-cmp', template: 'L' }) export class L { constructor(moduleRef: NgModuleRef<any>, appRef: ApplicationRef) { const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(M); let newNode = document.createElement('div'); newNode.id = 'placeholder'; document.body.prepend(newNode); const ref = factory.create(moduleRef.injector, [], newNode); appRef.attachView(ref.hostView); } } @Component({ selector: 'm-cmp', template: '{{title}}' }) export class M { title = 'I am the component that was created dynamically'; }

This is what we’ll see if we check the DOM structure in the application:

Content imageContent image

If we now take a look at _views property, this is what we’ll see:

Content imageContent image

We can use console to find out what those RootViewRef instances represent:

<>Copy
const TVIEW = 1; const CONTEXT = 8; const CHILD_HEAD = 13; const view_1 = appRef._views[0]; const view_2 = appRef._views[1]; view_1._lView[TVIEW].type // 0 - HostView view_1._lView[CONTEXT].constructor.name // M view_1._lView[CHILD_HEAD][TVIEW].type // 0 - HostView view_1._lView[CHILD_HEAD][CONTEXT].constructor.name // M view_2._lView[CONTEXT].constructor.name // AppComponent (RootView) view_2._lView[TVIEW].type // 0 - HostView view_2._lView[CHILD_HEAD][CONTEXT].constructor.name // AppComponent (ComponentView) view_2._lView[CHILD_HEAD][TVIEW].type // 1 - ComponentView view_2._lView[CHILD_HEAD][CHILD_HEAD][CONTEXT].constructor.name // L

The chart will show these relationships clearer:

Content imageContent image

Bootstrapping multiple root components
Link to this section

It’s possible to bootstrap multiple root components like this:

<>Copy
@NgModule({ declarations: [ AppComponent, AppRootAnother ], imports: [ BrowserModule ], bootstrap: [ AppComponent, AppRootAnother ] }) export class AppModule {}

which will create two root views and corresponding html tags:

Content imageContent image

The only thing to remember is that index.html should include tags for both selectors:

<>Copy
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>LearnAngular</title> </head> <body> <app-root></app-root> <app-root-another></app-root-another> </body> </html>

With this setup Angular will create two independent change detection trees. They will be registered under ApplicationRef._views and when ApplicationRef.tick() function will be called Angular will run change detection for both trees. This is in effect similar to using attachView. However, they will still be part of a single ApplicationRef, so they will share the injector defined for the AppModule.

For more in depth stuff like what you read above check out the course

Content imageContent image

If you believe something important is missing here do let me know in the comments!

Content imageContent image

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

author_image

About the author

Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

Looking for a JS job?
Job logo
Fullstack (Angular, Node.js) Developer

Nextian Corp.

Worldwide
Remote
$84k - $107k
Job logo
Application Developer (Angular)

Karsun Solutions, LLC

Worldwide
Remote
$104k - $132k
Job logo
Angular Developer

Ryan Consulting Group

Worldwide
Remote
$77k - $80k
More jobs

Featured articles

Angularpost
17 January 202323 min read
Improve page performance and LCP with NgOptimizedImage

Explore mechanisms of NgOptimizedImage directive to improve overall page performance, targeting especially the Largest Contentful Paint (LCP) metric from Core Web Vitals. Enhance pages, make the best user experience and improve the web.