web-dev-qa-db-ja.com

Angular 4-NG-IFの使用中に「式はチェック後に変更されました」エラー

ログインしたユーザーを追跡するためにserviceをセットアップします。そのサービスはObservableを返し、subscribeのすべてのコンポーネントに通知されます(これまでに単一のコンポーネントのみがサブスクライブします)

サービス:

private subject = new Subject<any>();

sendMessage(message: boolean) {
   this.subject.next( message );
}

getMessage(): Observable<any> {
   return this.subject.asObservable();
} 

ルートアプリコンポーネント:(このコンポーネントはオブザーバブルにサブスクライブします)

ngAfterViewInit(){
   this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; });
}

ようこそコンポーネント:

ngOnInit() {
  const checkStatus = this._authService.checkUserStatus();
  this._authService.sendMessage(checkStatus);
}

App Component Html:(ここでエラーが発生します)

<div *ngIf="user"><div>

私がやろうとしていること:

すべてのコンポーネント(を除くルートアプリコンポーネント)ユーザーのログイン状態をルートアプリコンポーネントに送信して、ルートアプリコンポーネントHtml内のUIを操作できるようにします。

問題:

Welcome Componentが初期化されると、次のエラーが表示されます。

Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'.

このエラーは、ルートアプリコンポーネントのHTMLファイル内にあるこの*ngIf="user"式で発生することに注意してください。

誰かがこのエラーの理由と私がこれを修正する方法を説明できますか?

補足:私がやろうとしていることを達成するためのより良い方法があると思うなら、私に知らせてください。

更新1:

以下をconstructorに入れると問題は解決しますが、constructorをこの目的に使用したくないので、良い解決策ではないようです。

ようこそコンポーネント:

constructor(private _authService: AuthenticationService) {
  const checkStatus = this._authService.checkUserStatus();
  this._authService.sendMessage(checkStatus);
 }

ルートアプリコンポーネント:

constructor(private _authService: AuthenticationService){
   this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; });
}

更新2:

plunkr です。エラーを確認するには、ブラウザコンソールを確認してください。アプリがロードされると、ブール値trueが表示されるはずですが、コンソールにエラーが表示されます。

このplunkrは私のメインアプリの非常に基本的なバージョンであることに注意してください。アプリが少し大きいので、すべてのコードをアップロードできませんでした。しかし、plunkrはエラーを完全に示しています。

13
Skywalker

これは、変更検出サイクル自体が変更を引き起こしたように見えることを意味します。これは、偶然(つまり、変更検出サイクルが何らかの原因で発生した)または意図的なものである可能性があります。意図的に変更検出サイクルで何かを変更する場合、ここでは行われていない変更検出の新しいラウンドを再トリガーする必要があります。このエラーはprodモードでは抑制されますが、コードに問題があり、不可解な問題が発生することを意味します。

この場合、特定の問題は、親に影響を与える子の変更検出サイクルで何かを変更していることであり、これにより、オブザーバブルのような非同期トリガーは通常、親の変更検出を再トリガーしません。親のサイクルを再トリガーしない理由は、これが単方向のデータフローに違反し、子が親の変更検出サイクルを再トリガーし、それが子を再トリガーし、次に親が再びトリガーするなどの状況を引き起こす可能性があるためですアプリの無限の変化検出ループ。

子は親コンポーネントにメッセージを送信できないと言っているように聞こえるかもしれませんが、そうではありません。問題は、変更検出サイクル中に子が親にメッセージを送信できないことですライフサイクルフックとして)、ユーザーイベントへの応答のように、外部で発生する必要があります。

ここでの最善の解決策は、更新の原因となっているコンポーネントの親ではない新しいコンポーネントを作成し、無限の変更検出ループを作成できないようにして、一方向のデータフローの違反を停止することです。これは、以下のplunkrで実証されています。

子が追加された新しいapp.component:

<div class="col-sm-8 col-sm-offset-2">
      <app-message></app-message>
      <router-outlet></router-outlet>
</div>

メッセージコンポーネント:

@Component({
  moduleId: module.id,
  selector: 'app-message',
  templateUrl: 'message.component.html'
})
export class MessageComponent implements OnInit {
   message$: Observable<any>;
   constructor(private messageService: MessageService) {

   }

   ngOnInit(){
      this.message$ = this.messageService.message$;
   }
}

テンプレート:

<div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div>

わずかに変更されたメッセージサービス(わずかにクリーンな構造):

@Injectable()
export class MessageService {
    private subject = new Subject<any>();
    message$: Observable<any> = this.subject.asObservable();

    sendMessage(message: string) {
       console.log('send message');
        this.subject.next(message);
    }

    clearMessage() {
       this.subject.next();
    }
}

これには、無限ループを作成するリスクなしに、変更検出を適切に機能させるよりも多くの利点があります。また、コードがよりモジュール化され、責任がより分離されます。

https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview

17
bryan60

いい質問だから、何が問題の原因なの?このエラーの理由は何ですか? Angular変更検出の仕組みを理解する必要があります。簡単に説明します。

  • プロパティをコンポーネントにバインドします
  • アプリケーションを実行します
  • イベントが発生します(タイムアウト、ajax呼び出し、DOMイベントなど)
  • バインドされたプロパティは、イベントの効果として変更されます
  • また、Angularはイベントをリッスンし、変更検出ラウンドを実行します
  • 角度はビューを更新します
  • AngularはライフサイクルフックngOnInitngOnChangesおよびngDoCheckを呼び出します
  • すべての子コンポーネントで変更検出ラウンドを実行します
  • AngularはライフサイクルフックngAfterViewInitを呼び出します

しかし、ライフサイクルフックにプロパティを再度変更するコードが含まれていて、Change Detection Roundが実行されない場合はどうでしょうか?または、ライフサイクルフックに別の変更検出ラウンドを引き起こすコードが含まれていて、そのコードがループに入った場合はどうなりますか?これは危険な結果であり、Angularは、プロパティに注意を払って、その間または直後に変更しないようにします。これは、2番目の変更検出ラウンド =最初の後に、何も変更されていないことを確認するために注意してください。

2つのイベントを同時に(または非常に短い時間枠で)トリガーすると、Angularは2つの変更検出サイクルを同時に起動します。この場合、問題はありません。 Angular両方のイベントがChange Detection RoundおよびAngularは何が起こっているのかを理解するのに十分インテリジェントです。

ただし、すべてのイベントが変更検出ラウンドを引き起こすわけではありません。その例は、Observableが変更検出戦略をトリガーしないことです。

あなたがしなければならないのは、目覚めさせることですAngular=変更検出のラウンドをトリガーします。EventEmitter、タイムアウト、whateverはイベントを発生させます。

私のお気に入りのソリューションはwindow.setTimeout

this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0));

これにより問題が解決します。

3

コンストラクターでこの行を宣言します

private cd: ChangeDetectorRef

その後、ngAfterviewInitでこのように呼び出します

ngAfterViewInit() {
   // it must be last line
   this.cd.detectChanges();
}

dOM要素のブール値は変更されないため、問題は解決します。そのため、例外をスローします

あなたのPlunkrの回答はこちらAppComponentで確認してください

import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { MessageService } from './_services/index';

@Component({
    moduleId: module.id,
    selector: 'app',
    templateUrl: 'app.component.html'
})

export class AppComponent implements OnDestroy, OnInit {
    message: any = false;
    subscription: Subscription;

    constructor(private messageService: MessageService,private cd: ChangeDetectorRef) {
        // subscribe to home component messages
        //this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; });
    }

    ngOnInit(){
      this.subscription = this.messageService.getMessage().subscribe(message =>{
         this.message = message
         console.log(this.message);
         this.cd.detectChanges();
      });
    }

    ngOnDestroy() {
        // unsubscribe to ensure no memory leaks
        this.subscription.unsubscribe();
    }
}
2
Piyush Patel

エラーを理解するには、以下をお読みください。

あなたのケースは_Synchronous event broadcasting_カテゴリーに分類されます:

このパターンは by this plunker で示されています。アプリケーションは、イベントを発行する子コンポーネントと、このイベントをリッスンする親コンポーネントを持つように設計されています。このイベントにより、親プロパティの一部が更新されます。そして、これらのプロパティは、子コンポーネントの入力バインディングとして使用されます。これは、間接的な親プロパティの更新でもあります。

あなたの場合、更新される親コンポーネントプロパティはuserであり、このプロパティは_*ngIf="user"_への入力バインディングとして使用されます。問題は、ライフサイクルフックからイベントを実行しているため、変更検出サイクルの一部としてイベントthis._authService.sendMessage(checkStatus)をトリガーしていることです。

この記事で説明されているように、このエラーを回避するには2つの一般的なアプローチがあります。

  • 非同期更新-これにより、変更検出プロセスの外部でイベントをトリガーできます
  • 変更検出の強制-これにより、現在の実行と検証ステージの間に追加の変更検出実行が追加されます

ライフサイクルフックからイベントをトリガーする必要がある場合は、まず質問に答える必要があります。コンポーネントコンストラクターに必要なすべての情報が揃っていれば、それは悪い選択肢ではないと思います。 AngularのConstructorとngOnInitの本質的な違い を参照してください。

あなたの場合、冗長な変更検出サイクルを回避するために、手動の変更検出ではなく、非同期イベントトリガーを使用します。

_ngOnInit() {
  const checkStatus = this._authService.checkUserStatus();
  Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus););
}
_

または、AppComponent内の非同期イベント処理を使用します。

_ngAfterViewInit(){
    this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message));
_

上記で示したアプローチは、 実装のngModel で使用されています。

しかし、どうしてthis._authService.checkUserStatus()が同期するのでしょうか?

NgOnInitの変数を変更せず、コンストラクターで変更します

constructor(private apiService: ApiService) {
 this.apiService.navbarVisible(false);
}
0
Googlian

Angular 4.xへの移行後に同じ問題に最近遭遇しました。簡単な解決策は、setTimeout(() => {}, 0) // notice the 0 it's intentionalChangeDetectionを引き起こすコードの各部分をラップすることです。 。

このように、life-cycle hookの後にemitをプッシュするため、変更検出エラーは発生しません。これはかなり汚い解決策であることは承知していますが、それは実行可能なクイックフィックスです。