Detached Views and Change Detection Control

In the previous chapter on manual control, we examined the first three methods available on the change detector service: detectChanges, checkNoChanges, and markForCheck. Now we turn our attention to the final two methods 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 performs a straightforward operation—it clears the LViewFlags.Attached flag on the component view:

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

This is accomplished by applying the bitwise NOT operator (~) which flips every bit in its operand, setting the value to 0. If we examine the Attached flag definition:

export const enum LViewFlags {
  Attached = 0b000001000000,
}

After applying bitwise NOT, the result becomes:

~LViewFlags.Attached === 0b111110111111;

During change detection, this flag acts as a gatekeeper, determining whether a particular component receives a check:

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;
}

As you might anticipate, reattach reverses this operation by setting LViewFlags.Attached back to 1:

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

Consider a component hierarchy structured as follows:

Image alt

We can detach component A through its ChangeDetectorRef:

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

After this operation, component A is excluded from change detection. The consequence cascades—since A is skipped, its descendants are likewise omitted, meaning the entire left branch (indicated in bronze) never undergoes checking. This bears repeating: despite detaching only A, components A1 and A2 also escape verification. Consequently, even if template expressions within these components change, no updates will appear on screen.

Here's a compact demonstration of detached component view behavior:

@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);
  }
}

During the initial check, the span renders the expected text—See if I change: false. After two seconds, when the changed property transitions from false to true, the displayed text remains unchanged. However, removing the line this.cd.detach() restores normal behavior.

Isolated Checks

A surprising aspect emerges when calling detectChanges: it triggers change detection for the current component irrespective of its attached state. This enables a pattern of isolated change detection—detach the component from the primary tree, then invoke detectChanges whenever verification is needed.

The following example illustrates this concept:

@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);
  }
}

Here, we detach the component from the main tree and modify the changed property. Though the property updates after one second, the component remains unchecked, so the screen doesn't reflect the change. After two seconds, calling detectChanges performs local change detection, bringing the new value to view.

When invoking detectChanges on a detached component, its child components and embedded views also receive examination.

Practical Applications

The Angular documentation presents a compelling scenario for implementing local change detection with the detach and reattach pair:

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 implementation appears as:

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;
}

Visually, it operates like this:

Image alt

Clicking the checkbox updates the LiveData component's input binding to either true or false. When true, the setter reattaches the component view to the change detection tree, making it eligible for the next global check. When false, the view becomes detached.

This pattern also functions with the ngOnChanges hook, which continues firing for LiveData even when its 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();
    }
  }
}

One crucial observation stands out: the reattach method affects only the current component. If its parent component lacks enabled change detection, reattachment produces no effect. Therefore, using OnChanges or input setters to attach the view works exclusively for the topmost component in a detached subtree—nested components below won't benefit, since change detection never reaches their parent.

An alternative approach leverages local change detection. The next example renders only when even values arrive from 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();
      }
    });
  }
}