20 May 2024
7 min

Running change detection – Manual control

Manual control

Even though Angular runs change detection automatically, sometimes you may need to run change detection manually.
This would be the case if a change detector of the component is detached from the component view or updates happen outside of Angular zone.

Angular has two ways to manually trigger change detection:

The tick method runs change detection for the entire application starting from the root component,
while detectChanges performs local change detection starting with the component corresponding
to an instance of ChangeDetectorRef and proceeds downwards.

Let’s explore both methods in more details.

tick

Angular
uses this method
to run application wide change detection when it gets notifications from NgZone about no outstanding microtasks remaining:

export class ApplicationRef {
  constructor() {
    this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
      next: () => {
        this._zone.run(() => {
          this.tick();
        });
      },
    });
  }
}

But the method tick itself doesn’t really have anything to do with zones or NgZone.
It just invokes change detection for the whole application.

If we look
under the hood,
we can see that it simply iterates all top level (root) views and triggers detectChanges on each view:

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 { ... }
	}
}

We can also see that in development mode, tick also runs checkNoChanges
that performs a second change detection cycle to ensure that no further changes are detected.
If additional changes are picked up during this second cycle,
it means that bindings in the app have side-effects that cannot be resolved in a single change detection pass.
In this case, Angular throws an error ExpressionChanged error,
since an Angular enforces unidirectional data flow.

To see tick method in action, let’s use the following example:

@Component({
  selector: 'i-cmp',
  template: `
    {{ title }}
    <button (click)="changeName()">Change name</button>
  `,
})
export class I {
  title = 'Original';

  changeName() {
    this.title = 'Updated';
  }
}

We have a click handler here that updates the title property. Nothing fancy.
Now let’s disable NgZone in main.ts like this:

platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' });

When we run the code, we can see that screen is not updated when we click on the button:

Image alt

Let’s now inject ApplicationRef and trigger tick method inside the handler:

@Component({
  selector: 'i-cmp',
  template: `
    {{ title }}
    <button (click)="changeName()">Change name</button>
  `,
})
export class I {
  constructor(private appRef: ApplicationRef) {}

  title = 'Original';

  changeName() {
    this.title = 'Updated';
    this.appRef.tick();
  }
}

Let’s test it:

Image alt

With this change, all works as expected.

Sometimes you may see that NgZone.run is recommended as way to run change detection globally.
But as explained in the
Autorun with zones
chapter, the run method simply evaluates
the callback function inside the Angular zone. There’s not explicit call to ApplicationRef.tick() inside.
This means that if there’s no event notification from Angular zone when the callback has finished executing,
change detection will not happen automatically.

detectChanges

This method is available on the change detector service that is created by Angular for each component.
It is used to explicitly process change detection and its side-effects for the tree of components
starting with the component that you trigger detectChanges() on.

This so-called local change detection cycle is useful in many situations besides triggering
change detection manually when automatic check is prevented.
For example, if you are changing the state in a component with more ancestors than descendants you may
get a performance increase by using detectChanges() since you aren’t unnecessarily running
change detection on the component’s ancestors.
Another case is detached change detectors that we’ll explore in detail in the
detached views
chapter.

Under the hood detectChanges simply calls
refreshView
function that we explored briefly in the
Operations
section:

export class ViewRef implements ChangeDetectorRef_interface {
  constructor(public _lView: LView, ...) {}

  detectChanges(): void {
    detectChangesInternal(this._lView[TVIEW], this._lView, this.context);
  }
}

export function detectChangesInternal(tView, lView, context, ...) {
  try {
    refreshView(tView, lView, tView.template, context);
  } catch (error) {.... } finally {... }
}

As you can see from the code excerpt above,
a Change Detector service is basically a shallow wrapper around a component container implemented through
LView.
When a ViewRef is created for components the LView associated with the component is injected into the constructor.
When ViewRef is created for an embedded view, the LView that it receives also describes the embedded view, not the component.

Here’s how you can imaging the hierarchy is for two instances of A component:

Image alt

Angular implements a different subtype of a ViewRef for each
type of views:
On the screenshot below, we can see that ViewRef is used for component views,
EmbeddedViewRef is used for embedded views and InternalViewRef is used for root/host views.:

Image alt

Using change detector service

To see detectChanges method in action, let’s use the previous example with a button.
This time, instead of the ApplicationRef we’ll inject ChangeDetectorRef:

export class I {
  constructor(private cdRef: ChangeDetectorRef) {}

  title = 'Original';

  changeName() {
    this.title = 'Updated';
    this.cdRef.detectChanges();
  }
}

Let’s test it:

Image alt

As you can see, all works as expected.

The only difference this time from using ApplicationRef.tick() is that change detection doesn’t run for ancestor components,
particularly for the root AppComponent:

@Component({
  selector: 'app-root',
  template: `<i-cmp></i-cmp>`,
})
export class AppComponent {}

That’s easy to check if we simply add logging behavior to the refreshView function:

Image alt

I’m using the conditional breakpoint here instead of logpoint because
I don’t want to log output when the type of the view is root. We’re only interested in component views.

That’s the logging output when using detectChanges:

Image alt

Just the I component checked.

And that’s when we use `ApplicationRef„:

Image alt

You can see here that change detection is triggered twice,
one for the regular change detection cycle and the other time is for checkNoChanges flow.

Surprising ngDoCheck behavior

There’s an unexpected behaviour related to detectChanges.
The ngDoCheck hook is not triggered for the component that you trigger detectChanges on.
This happens because lifecycle hooks are executed on child components when checking their parents,
not the current component on which the call is made.
See chapter on operations.

One of the reasons it’s designed like this is to allow manual control of OnPush logic from the ngDoCheck hook.
If a child component is defined as onPush, and no input bindings have changed, you still can call markForCheck()
from ngDoCheck of this child component to mark the component as dirty.

markForCheck

Change Detector service implements markForCheck method that’s very useful but somewhat confusing.
In contrast to detectChanges(), it does not run change detection,
but explicitly marks the current component (view) and its ancestors as changed (dirty).
Next time a change detection cycle runs for any of its ancestors, this marked component view is guaranteed be checked.
This means that if a view needs to be synchronously updated before some other action,
you’ll need to use detectChanges() as calling markForCheck() may not result in a timely update.

This markForCheck method is mostly used with components that use OnPushchange detection strategy. Components are normally marked as dirty (in need of rerendering) when either values for input bindings have changed or UI related events have fired in the component’s template. If none of these two triggers occurred, you need to explicitly call this method to ensure that component will be included in the check next time change detection happens.

Since Angular only checks object references, it may not detect objects property input change if the reference remain the same.
One solution to this is using immutable objects.
Another solution involves performing the required check inside ngDoCheck method and explicitly marking the view as dirty.

Let’s see this example. We have parent component O that adds an item to the items array,
but the reference to the array remains the same.
The child component O1 that’s defined as OnPush takes this array of times as input and renders it.
By default, Angular won’t detect the change and won’t update the view when O adds the item.
That’s why we check for the array’s length manually and use markForCheck in O1 to explicitly mark the component dirty:

@Component({
  selector: 'o-cmp',
  template: '<o1-cmp [items]="items"></o1-cmp>',
})
export class O {
  items = [1, 2, 3];

  constructor() {
    setTimeout(() => {
      this.items.push(4);
    }, 2000);
  }
}

@Component({
  selector: 'o1-cmp',
  template: '<div *ngFor="let item of items">{{item}}</div>',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class O1 {
  @Input() items = [];
  prevLength = 0;

  constructor(private cdRef: ChangeDetectorRef) {}

  ngDoCheck() {
    if (this.items.length !== this.prevLength) {
      this.prevLength = this.items.length;
      this.cdRef.markForCheck();
    }
  }
}

When the markForCheck method is called,
under the hood
Angular simply iterates upwards starting from the current component view
and enables checks for every parent component up to the root component:

export function markViewDirty(lView: LView): LView | null {
  while (lView) {
    lView[FLAGS] |= LViewFlags.Dirty;
    const parent = getLViewParent(lView);
    // Stop traversing up as soon as you find a root view
    // that wasn't attached to any container
    if (isRootView(lView) && !parent) {
      return lView;
    }
    // continue otherwise
    lView = parent!;
  }
  return null;
}

The most important part is this assignment:

lView[FLAGS] |= LViewFlags.Dirty;

That’s where the code uses boolean OR to set the LViewFlags.Dirty
flag:

export const enum LViewFlags {
  /** Whether this view has default change detection strategy (checks always) or onPush */
  CheckAlways = 0b00000010000,

  /** Whether or not this view is currently dirty (needing check) */
  Dirty = 0b00000100000,
}

The idea behind using boolean OR is that resulting bitmask value will be 1 if any of the inputs is 1:

1 | 1 = 1
0 | 1 = 1

Angular checks this flag inside refreshComponent function to determine whether a component requires the check.
The function itself is executed from refreshView that’s core to change detection:

function refreshComponent(hostLView, componentHostIdx): void {
  ...
  const tView = componentView[TVIEW];
  if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
    refreshView(tView, componentView, tView.template, componentView[CONTEXT]);
  }
}

The markForCheck method might also be useful for coalescing and avoiding the error thrown when
a new change detection cycle is triggered while being in the middle of the current check cycle.
If you can’t be sure that this isn’t he case, use cd.markForCheck().

When changes affect multiple components and you’re sure the change detection run will follow,
by calling markForCheck instead of detectChanges you’re essentially
reducing the number of times change detection will be called by coalescing future runs into one cycle.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.