web-dev-qa-db-ja.com

親コンポーネントの値を子コンポーネントから更新すると、ExpressionChangedAfterItHasBeenCheckedError in Angular

_ParentComponent > ChildComponent_とサービスの2つのコンポーネントがあります。 TitleService

ParentComponentは次のようになります:

_export class ParentComponent implements OnInit, OnDestroy {

  title: string;


  private titleSubscription: Subscription;


  constructor (private titleService: TitleService) {
  }


  ngOnInit (): void {

    // Watching for title change.
    this.titleSubscription = this.titleService.onTitleChange()
      .subscribe(title => this.title = title)
    ;

  }

  ngOnDestroy (): void {
    if (this.titleSubscription) {
      this.titleSubscription.unsubscribe();
    }
  }    

}
_

ChildComponentは次のようになります:

_export class ChildComponent implements OnInit {

  constructor (
    private route: ActivatedRoute,
    private titleService: TitleService
  ) {
  }


  ngOnInit (): void {

    // Updating title.
    this.titleService.setTitle(this.route.snapshot.data.title);

  }

}
_

アイデアは非常に単純です。ParentControllerは画面にタイトルを表示します。常に実際のタイトルをレンダリングするために、TitleServiceにサブスクライブし、イベントをリッスンします。タイトルが変更されると、イベントが発生し、タイトルが更新されます。

ChildComponentが読み込まれると、ルーターからデータを取得し(動的に解決されます)、TitleServiceにタイトルを新しい値で更新するように指示します。

問題は、この解決策がこのエラーを引き起こすことです:

_Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'._

変化検出ラウンドで値が更新されたようです。

より良い実装を得るためにコードを再配置する必要がありますか、それともどこかで別の変更検出を開始する必要がありますか?

  • setTitle()およびonTitleChange()呼び出しを尊敬されるコンストラクターに移動できますが、コンストラクターロジックで「重労働」を行うことは悪い習慣と見なされていることを読みました。ローカルプロパティの初期化。

  • また、タイトルは子コンポーネントによって決定される必要があるため、このロジックを子コンポーネントから抽出することはできませんでした。


更新

問題をよりよく示すために、最小限の例を実装しました。 GitHubリポジトリ にあります。

徹底的な調査の結果、問題はParentComponentで_*ngIf="title"_を使用した場合にのみ発生しました。

_<p>Parent Component</p>

<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>

<hr>

<app-child></app-child>
_
10
Slava Fomin II

記事 ExpressionChangedAfterItHasBeenCheckedErrorエラーについて知っておく必要があるすべて は、この動作を詳細に説明しています。

あなたの問題には2つの可能な解決策があります:

1)ngIfの前にapp-childを置きます。

<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>

2)非同期イベントを使用します。

export class TitleService {
  private titleChangeEmitter = new EventEmitter<string>(true);
                                                       ^^^^^^--------------

徹底的な調査の結果、問題は* ngIf = "title"を使用した場合にのみ発生しました。

説明している問題はngIfに固有のものではなく、入力後の変更検出中に同期的に更新される親入力に依存するカスタムディレクティブを実装することで簡単に再現できます。ディレクティブに渡されました

@Directive({
  selector: '[aDir]'
})
export class ADirective {
  @Input() aDir;

------------

<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError

それが実際に発生する理由は、変更の検出とコンポーネント/ディレクティブ表現に固有のAngular内部を十分に理解する必要があります。あなたはこれらの記事から始めることができます:

この回答ですべてを詳細に説明することはできませんが、ここに高レベルの説明があります。 ダイジェストサイクル Angularの間、子ディレクティブに対して特定の操作を実行します。そのような操作の1つは、入力を更新し、子ディレクティブ/コンポーネントでngOnInitライフサイクルフックを呼び出すことです。重要なのは、これらの操作が厳密な順序で実行されることです。

  1. 入力を更新する
  2. NgOnInitを呼び出す

これで、次の階層ができました。

parent-component
    ng-if
    child-component

また、上記の操作を実行する場合、Angularはこの階層に従います。したがって、現在Angularがparent-componentをチェックすると仮定します。

  1. ng-iftitle入力バインディングを更新し、初期値undefinedに設定します
  2. ng-ifについてはngOnInitを呼び出します。
  3. child-componentの更新は必要ありません
  4. child-componentのタイトルをDynamic Titleに変更するparent-componentに対してngOnIntiを呼び出します。

したがって、title=undefinedのプロパティを更新するときにAngularがng-ifを渡したが、変更の検出が終了すると、title=Dynamic Titleparent-componentがあるという状況になります。ここで、Angularは2番目のダイジェストを実行して、変更がないことを確認します。ただし、前のダイジェストでng-ifに渡されたものを現在の値と比較すると、変更が検出され、エラーがスローされます。

ng-ifテンプレートのchild-componentparent-componentの順序を変更すると、angularがparent-componentのプロパティを更新する前にng-ifのプロパティが更新される状況になります。

14
Maxim Koretskyi

ChangeDetectorRefを使用してみると、Angularが変更について手動で通知され、エラーはスローされません。
titleParentComponentを変更した後、detectChangesChangeDetectorRefメソッドを呼び出します。

export class ParentComponent implements OnInit, OnDestroy {

  title: string;
  private titleSubscription: Subscription;

  constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
  }

  public ngOnInit() {
    this.titleSubscription = this.titleService.onTitleChange()
      .subscribe(title => this.title = title);

    this.changeDetector.detectChanges();
  }

  public ngOnDestroy(): void {
    if (this.titleSubscription
    ) {
      this.titleSubscription.unsubscribe();
    }
  }

}
4
Unsinkable Sam

場合によっては、「迅速な」解決策はsetTimeout()を使用することです。すべての警告を考慮する必要がありますが(他の人が参照している記事を参照)、タイトルを設定するような単純なケースでは、これが最も簡単な方法です。

ngOnInit (): void {

  // Watching for title change.
  this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {

    setTimeout(() => { this.title = title; }, 0);

 });
}

変更検出のすべての複雑さをまだ理解していない場合は、これを最後の手段として使用してください。それから戻ってきて、あなたがいるときに別の方法でそれを修正してください:)

1
Simon_Weaver

私の場合、私はデータの状態を変更する-この回答では、ダイジェストサイクルのAngularInDepth.comの説明を読む必要があります--htmlレベル内、すべて私私がデータを処理する方法を次のように変更する必要がありました。

<div>{{event.subjects.pop()}}</div>

<div>{{event.subjects[0]}}</div>

要約:ポップ(配列の最後の要素を返し、データの状態を変更する)の代わりに、通常の方法で状態を変更せずにデータを取得して例外を防止しました。

0
Miko Chu