web-dev-qa-db-fra.com

Angular 4 - Erreur "L'expression a changé après sa vérification" lors de l'utilisation de NG-IF

Je configure une service pour suivre les utilisateurs connectés. Ce service renvoie un observable et tous les composants que subscribe lui attribuent sont notifiés (jusqu'à présent, un seul composant s'y est abonné)).

Un service:

private subject = new Subject<any>();

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

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

Composant d'application racine: (ce composant souscrit à l'observable)

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

Composant de bienvenue: 

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

App Component Html: (c'est l'endroit où l'erreur se produit)

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

Ce que j'essaie de faire:

Je veux que chaque composant (sauf le composant d'application racine) envoie l'état connecté des utilisateurs au composant d'application racine afin que je puisse manipuler l'interface utilisateur dans le code HTML du composant d'application racine.

Le problème:

Je reçois le message d'erreur suivant lors de l'initialisation du composant Welcome Component.

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

Veuillez noter que cette erreur se produit sur cette expression *ngIf="user" qui se trouve dans le fichier HTML des composants d’application racine. 

Quelqu'un peut-il expliquer la raison de cette erreur et comment je peux y remédier?

Sur une note de côté: Si vous pensez qu'il y a une meilleure façon de réaliser ce que j'essaie de faire, alors s'il vous plaît faites le moi savoir.

Mise à jour 1:

Placer les éléments suivants dans constructor résout le problème, mais ne souhaite pas utiliser constructor à cette fin. Il semble donc que ce ne soit pas une bonne solution.

Composant de bienvenue:

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

Composant racine:

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

Mise à jour 2:

Voici le plunkr . Pour voir l'erreur, vérifiez la console du navigateur. Lorsque l'application se charge, une valeur booléenne true doit être affichée, mais l'erreur se produit dans la console.

Veuillez noter que ce plunkr est une version très basique de mon application principale. Comme l'application est un peu grande, je ne pouvais pas télécharger tout le code. Mais le plunkr démontre parfaitement l'erreur.

13
Skywalker

Cela signifie que le cycle de détection de changement lui-même semble avoir provoqué un changement, qui peut avoir été accidentel (c'est-à-dire que le cycle de détection de changement l'a provoqué d'une manière ou d'une autre) ou intentionnel. Si vous modifiez volontairement quelque chose dans un cycle de détection de changement, cela devrait alors réactiver une nouvelle série de détection de changement, ce qui ne se produit pas ici. Cette erreur sera supprimée en mode prod, mais signifie que vous avez des problèmes dans votre code et que vous causez des problèmes mystérieux. 

Dans ce cas, le problème spécifique est que vous modifiez quelque chose dans le cycle de détection des modifications d'un enfant qui affecte le parent et que cela ne déclenche pas la détection des modifications du parent, même si les déclencheurs asynchrones comme les observables le font habituellement. La raison pour laquelle il ne redéclenche pas le cycle parent est parce que cela viole le flux de données unidirectionnel et peut créer une situation dans laquelle un enfant retriggera un cycle de détection de changement de parent, qui déclenchera ensuite le redéclenchement de l'enfant, puis à nouveau, etc. une boucle de détection de changement infinie dans votre application. 

Cela peut sembler comme si je disais qu'un enfant ne peut pas envoyer de messages à un composant parent, mais ce n'est pas le cas, le problème est qu'un enfant ne peut pas envoyer de message à un parent pendant un cycle de détection de changement (tel que cycle de vie), cela doit se passer à l’extérieur, par exemple en réponse à un événement de l’utilisateur.

La meilleure solution consiste à arrêter de violer le flux de données unidirectionnel en créant un nouveau composant qui n'est pas le parent du composant à l'origine de la mise à jour, de sorte qu'une boucle de détection de changement infinie ne puisse pas être créée. Ceci est démontré dans le plunkr ci-dessous.

Nouvelle app.component avec l'enfant ajouté:

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

composant du message:

@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$;
   }
}

modèle:

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

service de messagerie légèrement modifié (structure légèrement plus propre):

@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();
    }
}

Cela présente plus d'avantages que de simplement laisser la détection des modifications fonctionner correctement sans risque de créer des boucles infinies. Cela rend également votre code plus modulaire et isole mieux la responsabilité.

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

12
bryan60

Belle question, alors, quelle est la cause du problème? Quelle est la raison de cette erreur? Nous devons comprendre comment fonctionne la détection de changement angulaire, je vais expliquer brièvement:

  • Vous liez une propriété à un composant
  • Vous exécutez une application
  • Un événement se produit (délais d'attente, appels ajax, événements DOM, ...)
  • La propriété liée est modifiée en tant qu'effet de l'événement
  • Angular écoute également l'événement et exécute un Détection de changement
  • Angular met à jour la vue
  • Angular appelle les hooks de cycle de vie ngOnInit, ngOnChanges et ngDoCheck
  • Course angulaire a Change Detection Round dans tous les composants enfants
  • Appels angulaires des points d'ancrage du cycle de vie ngAfterViewInit

Mais que se passe-t-il si un hook de cycle de vie contient un code qui modifie à nouveau la propriété et que/ Détection de changement n'est pas exécuté? Ou si un hook de cycle de vie contient un code qui provoque un autre changement de détection et que le code entre dans une boucle? Ceci est une éventualité dangereuse et Angular l’empêche de prêter attention à la propriété de ne pas changer dans le temps ou juste après. Ceci est réalisé en effectuant un deuxième changement de détection après le premier, pour être sûr que rien ne soit changé. Faites attention: cela ne se produit qu'en mode développement.

Si vous déclenchez deux événements en même temps (ou dans un laps de temps très court), Angular lancera deux cycles de détection de changement en même temps et il n'y aura aucun problème dans ce cas, car Angular, car les deux événements déclenchent un Change Détection Round and Angular est suffisamment intelligent pour comprendre ce qui se passe.

Mais tous les événements ne provoquent pas un changement de détection , et le vôtre est un exemple: un observable ne déclenche pas la stratégie de détection de changement.

Ce que vous devez faire est de réveiller Angular en déclenchant une série de changements de détection. Vous pouvez utiliser une EventEmitter, un délai d'attente, tout ce que provoque un événement.

Ma solution préférée utilise window.setTimeout:

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

Cela résout le problème. 

2
Cristian Traìna

Déclarer cette ligne dans le constructeur 

private cd: ChangeDetectorRef

après cela, dans l'appel ngAfterviewInit comme ceci

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

cela résoudra votre problème car la valeur booléenne de l'élément DOM ne sera pas modifiée. donc son exception exception

Votre réponse Plunkr ici Vérifiez auprès de 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();
    }
}
1
Piyush Patel

Pour comprendre l'erreur, lisez:

Votre cas tombe dans la catégorie Synchronous event broadcasting:

Ce motif est illustré par ce plunker . L'application est conçu pour avoir un composant enfant émettant un événement et un parent composante écoute de cet événement. L'événement cause une partie du parent propriétés à mettre à jour. Et ces propriétés sont utilisées comme entrée obligatoire pour le composant enfant. C’est aussi un parent indirect mise à jour de la propriété.

Dans votre cas, la propriété du composant parent mise à jour est user et cette propriété est utilisée comme liaison d'entrée avec *ngIf="user". Le problème est que vous déclenchez un événement this._authService.sendMessage(checkStatus) dans le cadre du cycle de détection des modifications, car vous le faites depuis le cycle de vie.

Comme expliqué dans l'article, vous avez deux approches générales pour contourner cette erreur:

  • Asynchronous update - permet de déclencher un événement en dehors du processus de détection des modifications.
  • Forçage de la détection de changement - ceci ajoute un cycle de détection de changement supplémentaire entre l'exécution en cours et l'étape de vérification

Vous devez d’abord répondre à la question s’il est nécessaire de déclencher la connexion du cycle de vie. Si vous avez toutes les informations dont vous avez besoin pour le même dans le constructeur de composant, je ne pense pas que ce soit la mauvaise option. Voir La différence essentielle entre Constructor et ngOnInit dans Angular pour plus de détails.

Dans votre cas, je choisirais probablement le déclenchement d’événement asynchrone au lieu de la détection manuelle des modifications pour éviter les cycles de détection des modifications redondants:

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

ou avec le traitement d'événements asynchrones à l'intérieur de AppComponent:

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

La méthode que je viens de montrer est utilisée par ngModel dans la mise en œuvre .

Mais je me demande aussi comment se fait-il que this._authService.checkUserStatus() soit synchrone?

Ne changez pas la var dans ngOnInit, changez-le dans le constructeur 

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

J'ai récemment rencontré le même problème après la migration vers Angular 4.x. Une solution rapide consiste à envelopper chaque partie du code qui provoque la variable ChangeDetection dans setTimeout(() => {}, 0) // notice the 0 it's intentional.

De cette façon, la emit sera APRÈS le life-cycle hook et ne causera donc pas d'erreur de détection de changement. Bien que je sache qu’il s’agit d’une solution plutôt sale, c’est un solution viable .

0