20 May 2024
5 min

Running change detection – Detached views

Detached views

In the chapter
manual control
we learnt how and where to use first 3 methods of a change detector service:
detectChanges, checkNoChanges and markForCheck.
In this chapter we’ll explore the remaining two defined on the
interface:

class abstract ChangeDetectorRef {
  abstract detectChanges() : void
  abstract checkNoChanges() : void
  abstract markForCheck() : void

  abstract detach() : void
  abstract reattach() : void
}

The detach method simply updates the LViewFlags.Attached flag on a component view:

export class ViewRef implements ChangeDetectorRef {
  detach() {
    this._lView[FLAGS] &= ~LViewFlags.Attached;
  }
}

It sets it to 0 using bitwise NOT (~) operator. The way it works is that it inverts the bits of its operand.
So if Attached flag is defined like this:

export const enum LViewFlags {
  Attached = 0b000001000000,
}

When bitwise NOT is applied to it it becomes:

~LViewFlags.Attached === 0b111110111111;

This flag is checked during change detection to determine if a component should be checked:

function refreshComponent(hostLView, componentHostIdx) {
  const componentView = getComponentLViewByIndex(componentHostIdx, hostLView);
  if (viewAttachedToChangeDetector(componentView)) {
    // check the component
  }
}

export function viewAttachedToChangeDetector(view: LView) {
  return (view[FLAGS] & LViewFlags.Attached) === LViewFlags.Attached;
}

You could probably guess that reattach simply resets the LViewFlags.Attached to 1:

export class ViewRef implements ChangeDetectorRef {
  reattach(): void {
    this._lView[FLAGS] |= LViewFlags.Attached;
  }
}

Imagine you have a component tree that looks like this:

Image alt

We can detach A component using ChangeDetectorRef:

export class A {
  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }
}

Once we do that, A component won't be checked. But because it won't be checked, neither will its children,
so the entire left branch starting with A will be skipped (bronze colored components will not be checked).
It's worth stressing again – even though we detached component A, its child components A1 and A2 will not be checked as well.
This means that even if expressions change in templates of these components we won’t see it any updates on the screen.

Here is the small example demonstrating the behavior of the detached component views:

@Component({
  selector: 'a-cmp',
  template: ` <span>See if I change: {{ changed }}</span> `,
})
export class A {
  changed = 'false';

  constructor(public cd: ChangeDetectorRef) {
    setTimeout(() => this.cd.detach());

    setTimeout(() => {
      this.changed = 'true';
    }, 2000);
  }
}

The first time the component is checked the span will be rendered with the text that we expect See if I change: false.
Within two seconds when changed property is updated from false to true, the text we will see on the screen will not change.
However, if we remove this line this.cd.detach(), everything will work as expected.

Local checks

What might be unexpected is that calling detectChanges will run change detection for the current component regardless of its state.
This means that you can create local change detection
by detaching the component from the main change detection tree and use detectChanges to run the check on demand.

Here is an example to demonstrate that:

@Component({
  selector: 'j-cmp',
  template: ` <span>See if I change: {{ changed }}</span> `,
})
export class J {
  changed = 'false';

  constructor(public cd: ChangeDetectorRef) {
    setTimeout(() => this.cd.detach());

    // some async event occurred, but the component isn't checked
    // so the changed property value isn't reflected on the screen
    setTimeout(() => (this.changed = 'true'), 1000);

    // at some point we may decide run update the screen
    // and run change detection locally
    setTimeout(() => {
      this.cd.detectChanges();
    }, 2000);
  }
}

In this example we detach the component from the main change detection tree and update the changed property.
The property is updated within one second, but the component is not checked, so the change is not reflected on the screen.
After two seconds we call detectChanges to run change detection locally. This will update the screen with the new value.

When we run detectChanges for a detached component, both its child components and embedded views will be checked as well.

Use cases

Angular docs
describe a very interesting use case for the detach and reattach method to implement local change detection:

The following example defines a component with a large list of readonly data.
Imagine, the data changes constantly, many times per second. For performance reasons,
we want to check and update the list every five seconds. We can do that by detaching
the component's change detector and doing a local change detection check every five seconds.

The code looks like this:

let data = 1;

class DataProvider {
  data = 0;

  constructor() {
    setInterval(() => {
      this.data = data++;
    }, 500);
  }
}

@Component({
  selector: 'live-data',
  template: 'Data: {{dataProvider.data}}',
})
export class LiveData {
  constructor(
    private ref: ChangeDetectorRef,
    public dataProvider: DataProvider
  ) {}

  @Input()
  set live(value: boolean) {
    if (value) {
      this.ref.reattach();
    } else {
      this.ref.detach();
    }
  }
}

@Component({
  selector: 'app',
  providers: [DataProvider],
  template: `
    Live Update: <input type="checkbox" [(ngModel)]="live" />
    <live-data [live]="live"></live-data>
  `,
})
export class App1 {
  live = true;
}

Here’s how it looks:

Image alt

When we click on a checkbox, the input binding on the LiveData component is updated with the corresponding value of true or false.
If true, the setter logic attaches the component view to the change detection tree
so that it’s checked during the next global change detection run. If false, the view is detached.

This would also work with ngOnChanges hook which is still triggered for LiveData even if its component view is detached:

export class LiveData {
  constructor(
    private ref: ChangeDetectorRef,
    public dataProvider: DataProvider
  ) {}

  @Input()
  live: boolean;

  ngOnChanges() {
    if (this.live) {
      this.ref.reattach();
    } else {
      this.ref.detach();
    }
  }
}

Here’s the important observation. The reattach method enables checks only for the current component,
but if changed detection is not enabled for its parent component, it will have no effect.
It means that the approach with using either OnChanges or input setters
to attach view will only work for the top-most component in the detached branch.
It won’t work for nested components in the disabled branch because the change detection won’t run for their parent component.

We could implement similar version but using local change detection.
The following example only re-renders for even values emitted by dataProvider:

let data = 1;

class DataProvider {
  data = 0;

  constructor() {
    setInterval(() => {
      this.data = data++;
    }, 500);
  }
}

@Component({
  selector: 'live-data-a',
  template: 'Data: {{dataProvider.data}}',
})
export class LiveDataA {
  constructor(
    private cdRef: ChangeDetectorRef,
    public dataProvider: DataProvider
  ) {
    this.cdRef.detach();

    setInterval(() => {
      if (dataProvider.data % 2 === 0) {
        this.cdRef.detectChanges();
      }
    });
  }
}
Share this post

Sign up for our newsletter

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