LoginSignup
19
14

More than 5 years have passed since last update.

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

Posted at
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 でも紹介した内容でしたが、今回は解決策を増やして、コードを少し充実させました。

参考

19
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14