Angular OnPush Component注意すべきなどこる: 続き

前日の[記事](https://qiita.com/Jialipassion/items/3234c4d3f18645d9cd7b)でAngular OnPush Componentでいろいろ注意すべきなどころを書きました、この記事がさらにいくつ普段あまり使われていないケースを説明したいと思います。

1. ケース１：`componentRef.changeDetectorRef.detectChanges()` is confusing.

```
@component({
  selector: 'dynamic',
  template: `{{name}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Dynamic  {
  name = 'initial name';
}

@component({
  selector: 'my-app',
  template: `
    <b>Expected</b>: "initial name" changes to "changed name" after 2s<br>
    <b>Actual</b>:<br>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent  {
  constructor(private _vcRef: ViewContainerRef,
     private _cfr: ComponentFactoryResolver, private _injector: Injector) {}

  ngOnInit() {
    const cmptRef = this._cfr.resolveComponentFactory(Dynamic).create(this._injector);
    this._vcRef.insert(cmptRef.hostView);

    setTimeout(() => {
        cmptRef.instance.name = 'changed name';
        cmptRef.changeDetectorRef.detectChanges(); // this will not update the DOM.
        // cmptRef.injector.get(ChangeDetectorRef).detectChanges(); // this will update the DOM.
    }, 2000);
  }
}
```

このケースが`Dynamic` のOnPush Componentを追加して、このcomponentの`cmptRef.changeDetectorRef.detectChanges()` をコールしても、画面が更新されないという現象になります。
そして、もし`cmptRef.injector.get(ChangeDetectorRef).detectChanges();`で実行されたら、画面が更新されました。

その原因が`cmptRef.changeDetectorRef`がこのOnPush ComponentのchangeDetectorRefではなく、そのComponentを格納するPlaceholderのHostViewのchangeDetectorRef になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109004/2af5765b-d825-7a53-4c8c-e3e940430fba.png)

なので、このHostViewの`detectChanges()`をコールして、OnPush ComponentがまだDirtyではなくため、画面が更新されません。

2. ケース２：`ComponentFixture.detectChanges()` is confusing

```
const myComp = TestBed.createComponent(OnPushComponent);
myComp.componentInstance.abc = 123;
myComp.detectChanges() // Does not work

myComp.componentRef.injector.get(ChangeDetectorRef).detectChanges(); // This will work.
```

ケース１と似てるですが、テストにもこの現象が有ります。

3. ケース３：`ngDoCheck()`が実行されましたが, 実際`ChangeDetection`が実行されていません.

```
@Component({
  selector: "onpush",
  template: `
    onpush: {{ name }} <br />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPush implements DoCheck {
  name = "initial name";

  ngDoCheck() {
    console.log("docheck onpush");
  }
}

@Component({
  selector: "my-app",
  template: `
    <onpush></onpush>
    <button (click)="onClick()">Update onpush</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  @ViewChild(OnPush) onPushComp: OnPush;
  constructor() {}

  onClick() {
    this.onPushComp.name = 'new name';
  }
}
```

このケースの場合、OnPushが更新されないのは正しいですが、`ngDoCheck`が実行されたのが変です。
その理由が`HostView`の存在です、実際はComponentのLifecycle hooksが親のViewに所属していますため。

4. ケース４：`dev mode`で、OnPush Componentが`CheckNoChanges()`が実行されていません、つまりどのようなパターンでも`ExpressionChangedAfterItHasBeenCheckedError`になれません。

```
import {
  Component,
  NgModule,
  ViewChild,
  ChangeDetectionStrategy,
  DoCheck,
  Input, AfterViewInit
} from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";

@Component({
  selector: "onpush",
  template: `
    onpush: {{ name }} <br />
    {{ input }}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPush implements AfterViewInit {
  name = 'initial name';

  ngAfterViewInit() {
    this.name = 'updated name';
  }

}

@Component({
  selector: "my-app",
  template: `
    <b>Expected</b>: "initial name" changes to "new name" after button click<br />
    <b>Actual</b>:<br />
    <onpush></onpush>
  `,
  changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent {
  @ViewChild(OnPush) onPushComp: OnPush;

  constructor() {}
}
```

その原因が今Angularが`ExpressionChangedAfterItHasBeenCheckedError`を探知するため、開発モードで２回Change Detectionを実行しました。OnPushの場合、１回目でDirtyフラグをリセットして、２回目がDirtyではない状態になって、CheckNoChangesの処理が実行されなくなりました。

では、なぜ`HostView`という概念が存在しますか？ちょっといろいろなLegacyの原因があります、これを説明すると、すごく長文になりしょうですので、纏めると、

1. Directiveを設計するとき、DirectiveがHostElementがないので、DirectiveのHooksとかを一個PlaceholderのHostViewに置く設計になりました。
2. ComponentもDirectiveなので（ComponentがViewがあるDirective）、設計を統一するため、ComponentにもHostViewを持ちました。
3. 普通のComponentの場合、わざわざ新しいHostViewを作る必要がなく、Parent Component ViewをHostViewとして利用する
4. BootStrap/Dynamic Componentの場合、作成したとき、親が分からないため、新しく一個HostViewを作成されました。

ということになりました。

このHostViewが開発者意識させたくないものなので、開発者が直接触るケースがないはずですが、上記のケースでちょっとわかりにくいことが発生しました。

今これらの問題を解決するため、大幅にChange Detectionのデータ構造・ロジックを更新することが難しくて、これから、

1. Documentを強化して、これらのケースとWork Around案を明確する
2. 将来てきには、HostViewをなくす設計を検討する

ということになります。
今わたしのほうで１番目を対応中ですが、皆さんがこれらの問題をあうとき、この記事が参考になればいいと思います。

以上です、どうもありがとうございました。