コンポーネントのプロパティが現在の日時に依存している場合に、Angular2の「式はチェック後に変更されました」という例外を管理する方法


168

私のコンポーネントには、現在の日時に依存するスタイルがあります。私のコンポーネントには、次の関数があります。

  private fontColor( dto : Dto ) : string {
    // date d'exécution du dto
    let dtoDate : Date = new Date( dto.LastExecution );

    (...)

    let color =  "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";

    return color;
  }

lightnessAmp現在の日時から計算されます。dtoDate過去24時間以内であれば、色が変わります。

正確なエラーは次のとおりです。

確認後、表情が変わった。以前の値: 'hsl(123、80%、49%)'。現在の値: 'hsl(123、80%、48%)'

例外が開発モードで表示されるのは、値がチェックされたときだけです。チェックされた値が更新された値と異なる場合、例外がスローされます。

したがって、例外を防ぐために、次のフックメソッドで各ライフサイクルの現在の日時を更新しようとしました。

  ngAfterViewChecked()
  {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
  }

...しかし成功しませんでした。


回答:


357

変更後に変更検出を明示的に実行します。

import { ChangeDetectorRef } from '@angular/core';

constructor(private cdRef:ChangeDetectorRef) {}

ngAfterViewChecked()
{
  console.log( "! changement de la date du composant !" );
  this.dateNow = new Date();
  this.cdRef.detectChanges();
}

完璧なソリューション、ありがとうございます。ngOnChanges、ngDoCheck、ngAfterContentCheckedのフックメソッドでも動作することに気付きました。それで、最良の選択肢はありますか?
AnthonyBrenelière16年

27
それはユースケースによって異なります。コンポーネントが初期化されるときに何かを実行したい場合ngOnInit()は、通常は最初の場所です。コードがレンダリングされるDOMに依存しているngAfterViewInit()ngAfterContentInit()、次のオプションである場合。ngOnChanges()入力が変更されるたびにコードを実行する必要がある場合に適しています。ngDoCheck()カスタム変更検出用です。実際、何にngAfterViewChecked()使うのが一番いいのかわかりません。直前か直後に呼ばれると思いますngAfterViewInit()
ギュンターZöchbauer

2
@KushalJayswal申し訳ありませんが、あなたの説明では意味がありません。あなたが達成しようとしていることを示すコードを使って、新しい質問を作成することをお勧めします。StackBlitzの例が理想的です。
ギュンターZöchbauer

4
これは、コンポーネントの状態がブラウザに計算DOMプロパティに基づいている場合、また、優れたソリューションであるようにclientWidthなど、
ヨナ

1
ngAfterViewInitで使用するだけで、魅力的に機能しました。
フランシスコアルレオ

42

TL; DR

ngAfterViewInit() {
    setTimeout(() => {
        this.dateNow = new Date();
    });
}

これは回避策ですが、この問題をより良い方法で解決することが本当に難しい場合があるので、このアプローチを使用している場合に自分を責めないでください。大丈夫。

:最初の問題は、[ リンクを解決]、setTimeout()[ リンク ]


回避する方法

一般に、このエラーは通常、どこかに(親/子コンポーネントでも)追加した後に発生しますngAfterViewInit。だから最初の質問はあなた自身に尋ねることngAfterViewInitです-私はなしで生きることができますか?おそらく、コードをどこかに移動します(ngAfterViewChecked別の方法かもしれません)。

:[ リンク ]


また

また、ngAfterViewInitDOMに影響を与える非同期のものはこれを引き起こす可能性があります。また、パイプに演算子をsetTimeout追加することにより、またはdelay(0)パイプを使用して解決できます。

ngAfterViewInit() {
  this.foo$
    .pipe(delay(0)) //"delay" here is an alternative to setTimeout()
    .subscribe();
}

:[ リンク ]


いい読書

これをデバッグする方法とそれが起こる理由についての良い記事:リンク


2
選択したソリューションよりも遅いようです
Pete B

6
これは最善の解決策ではありませんが、これは常に機能します。選択した回答が常に機能するとは限りません(フックを正しく機能させるには、フックを十分に理解する必要があります)
Freddy Bonda

これが機能する正しい説明は何ですか、誰もが知っていますか?それは別のスレッド(非同期)で実行されるためですか?
knnhcn 2018年

3
@knnhcn javascriptには別のスレッドなどはありません。JSは本質的にシングルスレッドです。SetTimeoutは、タイマーが切れた後、しばらくして関数を実行するようにエンジンに指示するだけです。ここでは、タイマーは、実際にその魔法の鮫を行うには、角度のための十分な時間である最近のブラウザで4として扱われる0、次のとおりです。developer.mozilla.org/en-US/docs/Web/API/...
Kilves

27

githubの問題で @leocaseiroが述べたように。

簡単な修正を探している人のために3つの解決策を見つけました。

1)からngAfterViewInitへの移動ngAfterContentInit

2)#14748(コメント)で提案されngAfterViewCheckedChangeDetectorRefいるようにと組み合わせる

3)ngOnInit()のままChangeDetectorRef.detectChanges()にしますが、変更後に呼び出します。


1
誰もが最も推奨される解決策を文書化できますか?
Pipo

13

彼女はあなたが2つの解決策をとります!

1. ChangeDetectionStrategyをOnPushに変更します

このソリューションでは、基本的に角度を指定します。

変更の確認を停止します。必要なときだけやります

クイックフィックス:

コンポーネントを変更して使用します ChangeDetectionStrategy.OnPush

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
    // ...
}

これで、物事はもう機能していないようです。これは、今後、AngularをdetectChanges()手動で呼び出す必要があるためです。

this.cdr.detectChanges();

ChangeDetectionStrategyの権利を理解するのに役立つリンクを次に示します。https//alligator.io/angular/change-detection-strategy/

2. ExpressionChangedAfterItHasBeenCheckedErrorを理解する

このエラーの原因についてのtomonari_t 回答からの小さな抜粋を以下に示します。これを理解するのに役立つ部分のみを含めようとしました。

記事全体は、ここに示されているすべてのポイントに関する実際のコード例を示しています。

根本的な原因は、角度のあるライフサイクルです。

各操作の後、Angularは操作の実行に使用した値を記憶しています。これらは、コンポーネントビューのoldValuesプロパティに格納されます。

すべてのコンポーネントのチェックが完了した後、Angularは次のダイジェストサイクルを開始しますが、操作を実行する代わりに、現在の値を前のダイジェストサイクルで記憶した値と比較します。

次の操作は、ダイジェストサイクルでチェックされています。

子コンポーネントに渡される値が、これらのコンポーネントのプロパティを更新するために現在使用される値と同じであることを確認してください。

DOM要素の更新に使用される値が、これらの要素の更新に使用される値と同じであることを確認します。

すべての子コンポーネントをチェックします

したがって、比較された値が異なる場合、エラーがスローされます。、ブロガーのMax Koretskyiは次のように述べています。

原因は常に子コンポーネントまたはディレクティブです。

そして最後に、通常はこのエラーを引き起こす実際のサンプルをいくつか示します。

  • 共有サービス
  • 同期イベント放送
  • 動的コンポーネントのインスタンス化

すべてのサンプルはここにあります(plunkr)。私の場合、問題は動的コンポーネントのインスタンス化でした。

また、私自身の経験から、私は誰もがsetTimeoutソリューションを回避することを強くお勧めします。私の場合、「ほぼ」無限ループが発生しました(挑発する方法を示すつもりのない21の呼び出し)。

Angularのライフサイクルを常に念頭に置いて、別のコンポーネントの値を変更するたびにどのように影響を受けるかを考慮することをお勧めします。このエラーにより、Angularは次のように伝えています。

あなたはおそらくこれを間違った方法で行っています、あなたは正しいですか?

同じブログはまた言います:

多くの場合、修正は、正しい変更検出フックを使用して動的コンポーネントを作成することです

私のための短いガイドは、コーディング中に少なくとも次の2つのことを考慮することです(私は時間をかけてそれを補完するように努めます):

  1. 代わりに、子のコンポーネントから親コンポーネントの値を変更しないでください。親から変更してください。
  2. あなたが使用する場合@Input@Outputディレクティブは、コンポーネントが完全に初期化されない限り、lyfecycleの変更をトリガしないようにしてください。
  3. this.cdr.detectChanges();それらの不要な呼び出しは、特に多くの動的データを処理しているときに、より多くのエラーをトリガーする可能性があるため、避けてください
  4. の使用this.cdr.detectChanges();が必須の場合は、使用@Input, @Output, etcされている変数()が正しい検出フック(OnInit, OnChanges, AfterView, etc)で入力/初期化されていることを確認してください
  5. 可能であれば、非表示はなく削除します。これは、ポイント3および4に関連しています。

また

Angular Life Hookを完全に理解したい場合は、こちらの公式ドキュメントを読むことをお勧めします:


なぜをに変更しChangeDetectionStrategyOnPush修正したのか、まだ理解できません。単純なコンポーネントがあります[disabled]="isLastPage()"。そのメソッドはMatPaginator-ViewChildを読み取って戻りthis.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;ます。paginatorはすぐには使用できませんが、を使用してバインドされた後は使用できます@ViewChildChangeDetectionStrategy削除されたエラーを変更すると、以前と同じように機能が引き続き存在します。現在どのような欠点があるのか​​はわかりませんが、ありがとうございます!
イゴール

素晴らしい@Igor!使用の唯一の撤退OnPushは、this.cdr.detectChanges()コンポーネントを更新するたびに使用する必要があることです。あなたはすでにそれを使用していたと思います
Luis Limas

9

今回のケースでは、changeDetectionをコンポーネントに追加して修正し、ngAfterContentCheckedでdetectChanges()を呼び出して、次のようにコーディングします。

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {

  show = false;

  private subscription: Subscription;

  constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }

  ngOnInit() {
    this.subscription = this.spinnerService.spinnerState
      .subscribe((state: SpinnerState) => {
        this.show = state.show;
      });
  }

  ngAfterContentChecked(): void {
      this.changeDedectionRef.detectChanges();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

2

私はあなたが想像できる最高で最もきれいな解決策はこれだと思います:

@Component( {
  selector: 'app-my-component',
  template: `<p>{{ myData?.anyfield }}</p>`,
  styles: [ '' ]
} )
export class MyComponent implements OnInit {
  private myData;

  constructor( private myService: MyService ) { }

  ngOnInit( ) {
    /* 
      async .. await 
      clears the ExpressionChangedAfterItHasBeenCheckedError exception.
    */
    this.myService.myObservable.subscribe(
      async (data) => { this.myData = await data }
    );
  }
}

Angular 5.2.9でテスト済み


3
これはハッキーです。
JoeriShoeby

1
@JoeriShoeby上記の他のすべてのソリューションは、setTimeoutsまたは高度なAngularイベントに基づく回避策です...このソリューションは、すべての主要ブラウザーdeveloper.mozilla.org/en-US/docs/Web/JavaScript/Reference/…caniuse.com によってサポートされる純粋なES2017 です。 /#search = await
JavierFuentes、2018年

1
たとえば、ES2017の機能に依存できないAngular + Cordovaを使用してモバイルアプリケーション(Android API 17を対象とする)を構築している場合はどうなりますか?...ではない回避策、受け入れられた答えは解決策であることに注意してください
JoeriShoeby

@JoeriShoeby typescriptを使用してJSにコンパイルされるため、機能サポートの問題はありません。
biggbest 2018

@biggbestどういう意味ですか?TypescriptがJSにコンパイルされることは知っていますが、それでもすべてのJSが機能するとは限りません。JavaScriptはWebブラウザーで実行され、ブラウザーによって動作が異なることに注意してください。
JoeriShoeby 2018年

2

何度も使った小さな作業

Promise.resolve(null).then(() => {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
    this.cdRef.detectChanges();
});

主に「null」をコントローラーで使用する変数に置き換えます。


1

すでに多くの回答があり、変更検出に関する非常に優れた記事へのリンクがありますが、ここで2セントを提示したいと思いました。チェックは理由があると思うので、アプリのアーキテクチャについて考え、ビューの変更はBehaviourSubject、正しいライフサイクルフックを使用して処理できることを認識しました。だからここに私がソリューションのためにやったことです。

  • 私はサードパーティのコンポーネント(フルカレンダー)を使用していますが、Angular Materialも使用しているため、スタイリング用の新しいプラグインを作成しましたが、リポジトリをフォークしないとカレンダーヘッダーをカスタマイズできないため、ルックアンドフィールを取得するのは少し面倒でした。そしてあなた自身のものを転がします。
  • そのため、基礎となるJavaScriptクラスを取得することになり、コンポーネントの独自のカレンダーヘッダーを初期化する必要がありました。これには、ViewChild私の親がレンダリングされる前にをレンダリングする必要があります。これは、Angularが機能する方法ではありません。これが、テンプレートに必要な値をにラップした理由ですBehaviourSubject<View>(null)

    calendarView$ = new BehaviorSubject<View>(null);

次に、ビューがチェックされていることを確認できたら、その件名をの値で更新します@ViewChild

  ngAfterViewInit(): void {
    // ViewChild is available here, so get the JS API
    this.calendarApi = this.calendar.getApi();
  }

  ngAfterViewChecked(): void {
    // The view has been checked and I know that the View object from
    // fullcalendar is available, so emit it.
    this.calendarView$.next(this.calendarApi.view);
  }

次に、私のテンプレートでは、asyncパイプを使用します。変更検出によるハッキングやエラーはなく、スムーズに機能します。

詳細が必要かどうかお気軽にお問い合わせください。


1

エラーを回避するには、デフォルトのフォーム値を使用します。

ngAfterViewInit()でdetectChanges()を適用するという受け入れられた回答を使用する代わりに(これも私の場合のエラーを解決しました)、動的に必要なフォームフィールドのデフォルト値を保存して、後でフォームが更新されたときに、ユーザーが新しい必須フィールドをトリガーする(そして[送信]ボタンを無効にする)フォームのオプションを変更する場合、その有効性は変更されません。

これにより、私のコンポーネントにほんの少しのコードが保存され、私の場合、エラーは完全に回避されました。


これは本当に良い習慣であり、これにより不要な電話をする必要がthis.cdr.detectChanges()
なくなり

1

私は変数を宣言し、後で
それを使用してその値を変更したかったので、そのエラーが発生しましたngAfterViewInit

export class SomeComponent {

    header: string;

}

私が切り替えたことを修正する

ngAfterViewInit() { 

    // change variable value here...
}

ngAfterContentInit() {

    // change variable value here...
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.