web-dev-qa-db-fra.com

ExpressionChangedAfterItHasBeenCheckedError Explained

S'il vous plaît, expliquez-moi pourquoi j'ai toujours cette erreur: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

De toute évidence, je ne l’obtiens qu’en mode dev, cela n’arrive pas sur la version de production, mais c’est très ennuyeux et je ne comprends tout simplement pas les avantages d’une erreur dans mon environnement de développement qui n’apparaîtra pas dans prod - Probablement à cause de mon manque de compréhension.

Habituellement, le correctif est assez simple, je viens d’envelopper l’erreur causant le code dans un setTimeout comme ceci:

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

Ou forcer à détecter les changements avec un constructeur comme celui-ci: constructor(private cd: ChangeDetectorRef) {}:

this.isLoading = true;
this.cd.detectChanges();

Mais pourquoi est-ce que je rencontre constamment cette erreur? Je veux le comprendre afin que je puisse éviter ces correctifs de hacky à l'avenir.

135
Kevin LeStarge

Une bonne compréhension est venue une fois que j'ai compris les crochets angulaires du cycle de vie et leur relation avec la détection de changement.

J'essayais de demander à Angular de mettre à jour un indicateur global lié au *ngIf d'un élément et j'essayais de le modifier à l'intérieur du point d'ancrage du cycle ngOnInit() d'un autre composant. 

Selon la documentation, cette méthode est appelée une fois que Angular a déjà détecté des modifications:

Appelé une fois, après le premier ngOnChanges ().

Donc, la mise à jour de l'indicateur à l'intérieur de ngOnChanges() ne déclenchera pas la détection des modifications. Ensuite, une fois que la détection de changement s'est naturellement à nouveau déclenchée, la valeur de l'indicateur a changé et l'erreur est renvoyée.

Dans mon cas, j'ai changé ceci:

constructor(private globalEventsService: GlobalEventsService) {

}

ngOnInit() {
    this.globalEventsService.showCheckoutHeader = true;
}

Pour ça:

constructor(private globalEventsService: GlobalEventsService) {
    this.globalEventsService.showCheckoutHeader = true;
}

ngOnInit() {

}

et ça a résolu le problème :)

34
Kevin LeStarge

J'ai eu un problème similaire. En regardant la documentation sur les crochets lifecycle , j'ai changé ngAfterViewInit en ngAfterContentInit et cela a fonctionné.

67
onlyme

Cette erreur indique un problème réel dans votre application, il est donc logique de générer une exception.

Dans devMode, la détection de changement ajoute un tour supplémentaire après chaque détection de changement régulière pour vérifier si le modèle a été modifié.

Si le modèle a changé entre le tour de détection de changement normal et supplémentaire, cela signifie que

  • la détection de changement elle-même a provoqué un changement
  • une méthode ou un getter renvoie une valeur différente à chaque fois qu'il est appelé

qui sont toutes les deux mauvaises, car il n’est pas clair comment procéder car le modèle pourrait ne jamais se stabiliser. 

Si Angular exécute la détection de changement jusqu'à ce que le modèle se stabilise, il pourrait fonctionner indéfiniment . Si Angular n'exécute pas la détection de changement, la vue risque de ne pas refléter l'état actuel du modèle.

Voir aussi Quelle est la différence entre la production et le mode de développement dans Angular2?

58
Günter Zöchbauer

Mettre à jour

Je recommande fortement de commencer par l'auto-réponse du PO first: réfléchissez correctement à ce qui peut être fait dans constructor et à ce qui devrait être fait dans ngOnChanges().

Original

C'est plus une note de côté qu'une réponse, mais cela pourrait aider quelqu'un. Je suis tombé sur ce problème en essayant de faire dépendre la présence d’un bouton de l’état de la forme:

<button *ngIf="form.pristine">Yo</button>

Autant que je sache, cette syntaxe conduit à l'ajout et à la suppression du bouton du DOM en fonction de la condition. Ce qui à son tour mène à la ExpressionChangedAfterItHasBeenCheckedError.

La solution dans mon cas (bien que je ne prétende pas saisir toutes les implications de la différence), consistait à utiliser display: none à la place:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>
26
Arnaud P

Dans mon cas, j'ai eu ce problème dans mon fichier de spécifications, lors de l'exécution de mes tests. 

Je devais changer ngIf en [hidden] 

<app-loading *ngIf="isLoading"></app-loading>

à

<app-loading [hidden]="!isLoading"></app-loading>
14
Andre Evangelista

Suivez les étapes ci-dessous:

1 . Utilisez 'ChangeDetectorRef' en l'important depuis @ angular/core comme suit:

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

2 . Implémentez-le dans constructeur () comme suit:

constructor(   private cdRef : ChangeDetectorRef  ) {}

3 . Ajoutez à votre fonction la méthode suivante que vous appelez lors d'un événement tel qu'un clic sur un bouton. Alors ça ressemble à ça: 

functionName() {   
    yourCode;  
    //add this line to get rid of the error  
    this.cdRef.detectChanges();     
}
12

Je faisais face au même problème car la valeur changeait dans l'un des tableaux de mon composant. Mais au lieu de détecter les modifications apportées aux modifications de valeur, j'ai modifié la stratégie de détection de modification de composant en onPush (qui détectera les modifications apportées aux modifications d'objet et non aux modifications de valeur). 

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush
    selector: -
    ......
})
10
Dheeraj

Il y avait des réponses intéressantes mais je ne semblais pas en trouver une qui corresponde à mes besoins, la plus proche étant celle de @ chittrang-mishra qui ne fait référence qu'à une fonction spécifique et non à plusieurs bascules comme dans mon application.

Je ne voulais pas utiliser [hidden] pour tirer avantage de *ngIf même si je ne fais pas partie du DOM. J'ai donc trouvé la solution suivante qui peut ne pas être la meilleure solution car elle supprime l'erreur au lieu de la corriger, mais dans mon cas où sais que le résultat final est correct, il semble bien pour mon application. 

Ce que j’ai fait, c’est d’appliquer AfterViewChecked, d’ajouter constructor(private changeDetector : ChangeDetectorRef ) {}, puis 

ngAfterViewChecked(){
  this.changeDetector.detectChanges();
}

J'espère que cela aide les autres comme beaucoup d'autres m'ont aidé.

9
eper

En se référant à l'article https://blog.angularindepth.com/everything-you-need-tknowknow-about-the-expressionchangedafterit bebeencheckederror-error-e3fd9ce7dbb4

Les mécanismes de détection des modifications fonctionnent donc de manière à ce que les analyses de détection des modifications et de vérification soient effectuées de manière synchrone. Cela signifie que si nous mettons à jour les propriétés de manière asynchrone, les valeurs ne seront pas mises à jour pendant l'exécution de la boucle de vérification et nous n'obtiendrons pas d'erreur ExpressionChanged.... La raison pour laquelle nous obtenons cette erreur est qu’au cours du processus de vérification, Angular voit des valeurs différentes de celles qu’il a enregistrées pendant la phase de détection des changements. Donc pour éviter ça ....

1) Utilisez changeDetectorRef

2) utilisez setTimeOut. Ceci exécutera votre code dans une autre VM en tant que macro-tâche. Angular ne verra pas ces modifications pendant le processus de vérification et vous n'obtiendrez pas cette erreur.

 setTimeout(() => {
        this.isLoading = true;
    });

3) Si vous voulez vraiment exécuter votre code sur la même VM utilisation comme 

Promise.resolve(null).then(() => this.isLoading = true);

Cela va créer une micro-tâche. La file d'attente de micro-tâches est traitée une fois l'exécution du code synchrone en cours. Par conséquent, la mise à jour de la propriété a lieu après l'étape de vérification.

9
ATHER

Angular exécute la détection des modifications et s’aperçoit que certaines valeurs transmises au composant enfant ont été modifiées. Angular renvoie l’erreur ExpressionChangedAfterItHasBeenCheckedError cliquez pour plus

Afin de corriger cela, nous pouvons utiliser le hook de cycle de vie AfterContentChecked et

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

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }
5
Lijo

Mon problème était manifeste lorsque j'ai ajouté *ngIf, mais ce n'était pas la cause. L'erreur était due à la modification du modèle dans les balises {{}}, puis à la tentative d'affichage ultérieure du modèle modifié dans l'instruction *ngIf. Voici un exemple:

<div>{{changeMyModelValue()}}</div> <!--don't do this!  or you could get error: ExpressionChangedAfterItHasBeenCheckedError-->
....
<div *ngIf="true">{{myModel.value}}</div>

Pour résoudre le problème, j'ai changé l'endroit où j'ai appelé changeMyModelValue () à un endroit plus logique. 

Dans ma situation, je voulais que changeMyModelValue () soit appelé chaque fois qu'un composant enfant modifie les données. Cela nécessitait la création et l'émission d'un événement dans le composant enfant afin que le parent puisse le gérer (en appelant changeMyModelValue (). See https://angular.io/guide/component-interaction#parent-listens-for-child- un événement

1
goku_da_master

Dans mon cas, j'avais une propriété asynchrone dans LoadingService avec un BehavioralSubject isLoading

L'utilisation du modèle [hidden] fonctionne, mais * ngIf échoue

    <h1 [hidden]="!(loaderService.isLoading | async)">
        THIS WORKS FINE
        (Loading Data)
    </h1>

    <h1 *ngIf="!(loaderService.isLoading | async)">
        THIS THROWS ERROR
        (Loading Data)
    </h1>
1
Mahesh

Voici mes pensées sur ce qui se passe. Je n'ai pas lu la documentation, mais je suis sûr que cela explique en partie l'erreur. 

*ngIf="isProcessing()" 

Lors de l'utilisation de * ngIf, le DOM est modifié physiquement en ajoutant ou en supprimant l'élément à chaque modification de la condition. Ainsi, si la condition change avant d'être rendue à la vue (ce qui est hautement possible dans le monde d'Angular), l'erreur est renvoyée. Voir explication ici entre les modes de développement et de production.

[hidden]="isProcessing()"

Lorsque vous utilisez [hidden], cela ne modifie pas physiquement le DOM, mais masque simplement l'élément de la vue, utilisant probablement CSS à l'arrière. L'élément est toujours présent dans le DOM mais n'est pas visible en fonction de la valeur de la condition. C'est pourquoi l'erreur ne se produira pas si vous utilisez [hidden].

1
Kobus

Une solution qui a fonctionné pour moi avec rxjs

import { startWith, tap, delay } from 'rxjs/operators';

// Data field used to populate on the html
dataSource: any;

....

ngAfterViewInit() {
  this.yourAsyncData.
      .pipe(
          startWith(null),
          delay(0),
          tap((res) => this.dataSource = res)
      ).subscribe();
}
1
Sandeep K Nair

Pour mon problème, je lisais github - "ExpressionChangedAfterItHasBeenCheckedError lors de la modification d'une valeur" non modèle "du composant dans afterViewInit" et j'ai décidé d'ajouter le ngModel

<input type="hidden" ngModel #clientName />

Cela a résolu mon problème, j'espère que cela aidera quelqu'un.

1
Demodave

J'ai eu cette erreur parce que j'utilisais une variable dans composant.html qui n'a pas été déclarée dans composant.ts. Une fois que j'ai supprimé la pièce en HTML, cette erreur a disparu.

0
shreekar hegde

@HostBinding peut être une source source de confusion de cette erreur.

Par exemple, supposons que vous ayez la liaison d’hôte suivante dans un composant

// image-carousel.component.ts
@HostBinding('style.background') 
style_groupBG: string;

Pour simplifier, disons que cette propriété est mise à jour via la propriété d'entrée suivante:

@Input('carouselConfig')
public set carouselConfig(carouselConfig: string) 
{
    this.style_groupBG = carouselConfig.bgColor;   
}

Dans le composant parent, vous le définissez par programme dans ngAfterViewInit

@ViewChild(ImageCarousel) carousel: ImageCarousel;

ngAfterViewInit()
{
    this.carousel.carouselConfig = { bgColor: 'red' };
}

Voici ce qui se passe:

  • Votre composant parent est créé
  • Le composant ImageCarousel est créé et affecté à carousel (via ViewChild)
  • Nous ne pouvons pas accéder à carousel jusqu'à ngAfterViewInit() (ce sera null)
  • Nous assignons la configuration, qui définit style_groupBG = 'red'
  • Ceci à son tour définit background: red sur le composant Host ImageCarousel
  • Ce composant est la propriété de votre composant parent. Ainsi, lorsqu'il vérifie les modifications, il trouve une modification dans carousel.style.background et n'est pas assez intelligent pour savoir que cela ne pose pas de problème, il lève l'exception.

Une solution consiste à introduire un autre diviseur initié dans ImageCarousel et à définir la couleur d'arrière-plan, mais vous n'obtenez pas certains des avantages de l'utilisation de HostBinding (par exemple, en permettant au parent de contrôler toutes les limites de l'objet).

La meilleure solution, dans le composant parent, consiste à ajouter detectChanges () après avoir défini la configuration.

ngAfterViewInit()
{
    this.carousel.carouselConfig = { ... };
    this.cdr.detectChanges();
}

Cela peut sembler assez évident et ressembler beaucoup à d’autres réponses, mais il ya une différence subtile.

Prenons le cas où vous n'ajoutez pas @HostBinding jusqu'à plus tard au cours du développement. Soudain, vous obtenez cette erreur et cela ne semble avoir aucun sens.

0
Simon_Weaver

J'ai eu cette erreur parce que je répartissais les actions redux en mode modal et que le mode modal n'était pas ouvert à ce moment-là. Je transmettais des actions au moment où la composante modale reçoit une entrée. Donc, je mets setTimeout là-bas afin de s'assurer que modal est ouvert et ensuite les actions sont dipatched.

0
Muneem Habib

La solution ... services et rxjs ... les émetteurs d'événements et les liaisons de propriété utilisent tous deux rxjs..Vous avez intérêt à l'implémenter vous-même, plus de contrôle, plus de débogage. Rappelez-vous que les émetteurs d'événements utilisent rxjs. Créez simplement un service et, dans un observable, inscrivez chaque composant à l'observateur et transmettez-lui une nouvelle valeur ou une nouvelle valeur si nécessaire.

0
user998548

J'avais un bloc de code dans la portée d'abonnement, je viens de le changer en portée supérieure et cela fonctionne bien.

0

Astuces de débogage

Cette erreur peut être assez déroutante et il est facile de faire une fausse hypothèse sur le moment exact où elle se produit. Je trouve utile d’ajouter de nombreuses instructions de débogage comme celle-ci dans les composants affectés aux endroits appropriés. Cela aide à comprendre le flux.

Dans les instructions parent mises comme ceci (la chaîne exacte 'EXPRESSIONCHANGED' est importante), mais à part cela, ce ne sont que des exemples:

    console.log('EXPRESSIONCHANGED - HomePageComponent: constructor');
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config', newConfig);
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config ok');
    console.log('EXPRESSIONCHANGED - HomePageComponent: running detectchanges');

Dans les rappels enfant/services/minuterie:

    console.log('EXPRESSIONCHANGED - ChildComponent: setting config');
    console.log('EXPRESSIONCHANGED - ChildComponent: setting config ok');

Si vous exécutez detectChanges, ajoutez également la journalisation manuellement:

    console.log('EXPRESSIONCHANGED - ChildComponent: running detectchanges');
    this.cdr.detectChanges();

Ensuite, dans Chrome debugger, il suffit de filtrer par "EXPRESSIONCHANGES". Cela vous montrera exactement le flux et l'ordre de tout ce qui est défini, et aussi exactement à quel point Angular lève l'erreur.

 enter image description here

Vous pouvez également cliquer sur les liens gris pour insérer des points d'arrêt.

Une autre chose à surveiller si vous avez des propriétés nommées de la même manière dans votre application (telle que style.background) est de veiller à déboguer celle que vous pensez, en lui attribuant une valeur de couleur obscure.

0
Simon_Weaver

J'espère que cela aidera quelqu'un venant ici: nous faisons des appels de service dans ngOnInit de la manière suivante et utilisons une variable displayMain pour contrôler le montage des éléments dans le DOM.

composant.ts

  displayMain: boolean;
  ngOnInit() {
    this.displayMain = false;
    // Service Calls go here
    // Service Call 1
    // Service Call 2
    // ...
    this.displayMain = true;
  }

et composant.html

<div *ngIf="displayMain"> <!-- This is the Root Element -->
 <!-- All the HTML Goes here -->
</div>
0
retr0