[Angular] ExpressionChangedAfterItHasBeenCheckedError への2つの対処法


ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'something: null'. Current value: 'something: something'.

Componentを入れ子構造にしているとよく見るこのエラー
どうも、子供のComponentから親Componentを変更しようとすると発生するようだ。
Angularでは、子供のComponentからの親の変更を基本的に禁止しているので、こういうことが出るらしい。

とりあえずこの2つのどっちかでOK

  1. setTimeout による非同期処理化
  2. detectChanged による明示的な変更

今回の例の前提と登場人物

子Componentにより、親ComponentのViewにあるタイトルが変更されるとする。

  1. 親 Component (ParentComponent)
  2. 子 Component (ChildComponent)
  3. タイトルを司るサービス(TitleService)

ParentComponent

Typescript

@Component({
  selector: 'app-parent',
  templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
  title: string;
  constructor(
    private titleService: TitleService,
  ) {}
  ngOnInit(): void {
    this.titleService.value.subscribe((value) => {
      this.title = value;
    });
  }
}

View

<div>
  <h1>{{ title }} </h1>
  <ng-content></ng-content>
</div>

ChildComponent

@Component({
  selector: 'app-child',
  templateUrl: 'app-child.component.html',
})
export class ChildComponent implements OnInit {
  constructor(
    private titleService: TitleService,
  ) {}
  ngOnInit(): void {
    this.titleService.value.next('子供');
  }
}

TitleService

@Injectable()
export class TitleService {
  value: BehaviorSubject<string> = new BehaviorSubject(null);
}

上記コードを実行すると、

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'value: null'. Current value: 'value: 子供'.

というエラーが出る。

setTimeout による非同期化

これは非常に簡単。でも、なんで setTimeout するんだ?という一見謎のコードになる。

@Component({
  selector: 'app-parent',
  templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
  title: string;
  constructor(
    private titleService: TitleService,
  ) {}
  ngOnInit(): void {
    this.titleService.value.subscribe((value) => {
      setTimeout(() => {
        this.title = value;
      });
    });
  }
}

不自然なコードになり、メンテナンス性が下がるので、個人的には下記をおすすめする

detectChanged による明示的な変更

@Component({
  selector: 'app-parent',
  templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
  title: string;
  constructor(
    private titleService: TitleService,
    private cd: ChangeDetectorRef,
  ) {}
  ngOnInit(): void {
    this.titleService.value.subscribe((value) => {
      this.title = value;
      this.cd.detectChanges();
    });
  }
}

DIするサービスが一つ増えるので、面倒になるのだが、こちらは何をやっているかが明確(なぜやっているかは明確ではないけど)

最後に

どちらの場合でも、エラーは消えるはずです。複数人でメンテし続けるなら、後者が良いかなと思いますが、速さ重視の個人プロダクトなら、前者もありかと思います。

この内容 https://qiita.com/seteen/items/16246f6351c1c4cb281b でも紹介した内容でしたが、今回は解決策を増やして、コードを少し充実させました。

参考