Comment gérer l'exception Angular2 «expression a changé après avoir été vérifiée» lorsqu'une propriété de composant dépend de la date / heure actuelle

168

Mon composant a des styles qui dépendent de la date / heure actuelle. Dans mon composant, j'ai la fonction suivante.

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

lightnessAmpest calculé à partir de la date / heure actuelle. La couleur change si dtoDatec'est dans les dernières 24 heures.

L'erreur exacte est la suivante:

L'expression a changé après avoir été vérifiée. Valeur précédente: 'hsl (123, 80%, 49%)'. Valeur actuelle: 'hsl (123, 80%, 48%)'

Je sais que l'exception n'apparaît en mode développement qu'au moment où la valeur est vérifiée. Si la valeur vérifiée est différente de la valeur mise à jour, l'exception est levée.

J'ai donc essayé de mettre à jour la date et l'heure actuelle à chaque cycle de vie dans la méthode de crochet suivante pour éviter l'exception:

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

... mais sans succès.

Anthony Brenelière
la source

Réponses:

357

Exécutez la détection des modifications explicitement après la modification:

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();
}
Günter Zöchbauer
la source
Solution parfaite, merci. J'ai remarqué que cela fonctionne également avec les méthodes de hook suivantes: ngOnChanges, ngDoCheck, ngAfterContentChecked. Alors, y a-t-il une meilleure option?
Anthony Brenelière
28
Cela dépend de votre cas d'utilisation. Si vous voulez faire quelque chose quand un composant est initialisé, ngOnInit()c'est généralement le premier endroit. Si le code dépend du DOM en cours de rendu ngAfterViewInit()ou ngAfterContentInit()des options suivantes. ngOnChanges()est un bon ajustement si le code doit être exécuté à chaque fois qu'une entrée a été modifiée. ngDoCheck()est pour la détection de changement personnalisé. En fait, je ne sais pas ce qui ngAfterViewChecked()est le mieux utilisé. Je pense que ça s'appelle juste avant ou après ngAfterViewInit().
Günter Zöchbauer
2
@KushalJayswal désolé, cela n'a pas de sens d'après votre description. Je suggère de créer une nouvelle question avec le code qui montre ce que vous essayez d'accomplir. Idéalement avec un exemple StackBlitz.
Günter Zöchbauer
4
C'est également une excellente solution si l'état de votre composant est basé sur les propriétés DOM calculées par le navigateur, comme clientWidth, etc.
Jonah
1
Juste utilisé sur ngAfterViewInit, a fonctionné comme un charme.
Francisco Arleo
42

TL; DR

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

Bien que ce soit une solution de contournement, il est parfois très difficile de résoudre ce problème de manière plus agréable, alors ne vous blâmez pas si vous utilisez cette approche. C'est bon.

Exemples : Le problème initial [ lien ], résolu avec setTimeout()[ lien ]


Comment éviter

En général, cette erreur se produit généralement après avoir ajouté quelque part (même dans les composants parents / enfants) ngAfterViewInit. La première question est donc de vous poser la question: puis-je vivre sans ngAfterViewInit? Peut-être que vous déplacez le code quelque part ( ngAfterViewCheckedpeut être une alternative).

Exemple : [ lien ]


Aussi

Des éléments asynchrones ngAfterViewInitqui affectent le DOM peuvent également en être la cause. Peut également être résolu via setTimeoutou en ajoutant l' delay(0)opérateur dans le tube:

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

Exemple : [ lien ]


Bonne lecture

Bon article sur la façon de déboguer et pourquoi cela se produit: lien

Sergueï Panfilov
la source
2
Semble être plus lent que la solution choisie
Pete B
6
Ce n'est pas la meilleure solution, mais putain cela fonctionne TOUJOURS. La réponse choisie ne fonctionne pas toujours (il faut bien comprendre le crochet pour le faire fonctionner)
Freddy Bonda
Quelle est la bonne explication pour laquelle cela fonctionne, tout le monde le sait? Est-ce parce qu'il s'exécute ensuite dans un thread différent (asynchrone)?
knnhcn
3
@knnhcn Il n'y a pas de thread différent en javascript. JS est par nature monothread. SetTimeout dit simplement au moteur d'exécuter la fonction quelque temps après l'expiration du temporisateur. Ici, le minuteur est 0, qui est en fait traité comme 4 dans les navigateurs modernes, ce qui laisse largement le temps à Angular de faire ses trucs magiques: developer.mozilla.org/en-US/docs/Web/API
...
27

Comme mentionné par @leocaseiro sur le problème de github .

J'ai trouvé 3 solutions pour ceux qui recherchent des solutions faciles.

1) Passer de ngAfterViewInitàngAfterContentInit

2) Déplacement vers ngAfterViewCheckedcombiné avec ChangeDetectorRefcomme suggéré sur # 14748 (commentaire)

3) Continuez avec ngOnInit () mais appelez ChangeDetectorRef.detectChanges()après vos modifications.

candideJ
la source
1
Quelqu'un peut-il documenter quelle est la solution la plus recommandée?
Pipo
13

Son vous allez deux solutions!

1. Modifiez ChangeDetectionStrategy en OnPush

Pour cette solution, vous dites essentiellement angulaire:

Arrêtez de vérifier les changements; je le ferai seulement quand je saurai que c'est nécessaire

La solution rapide:

Modifiez votre composant pour qu'il utilise ChangeDetectionStrategy.OnPush

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

Avec cela, les choses ne semblent plus fonctionner. En effet, à partir de maintenant, vous devrez effectuer un appel Angular detectChanges()manuellement.

this.cdr.detectChanges();

Voici un lien qui m'a aidé à comprendre ChangeDetectionStrategy à droite: https://alligator.io/angular/change-detection-strategy/

2. Comprendre ExpressionChangedAfterItHasBeenCheckedError

Voici un petit extrait de la réponse de tomonari_t sur les causes de cette erreur, j'ai essayé de n'inclure que les parties qui m'ont aidé à comprendre cela.

L'article complet montre de vrais exemples de code sur chaque point montré ici.

La cause première est le cycle de vie angulaire:

Après chaque opération, Angular se souvient des valeurs utilisées pour effectuer une opération. Ils sont stockés dans la propriété oldValues ​​de la vue du composant.

Une fois que les vérifications ont été effectuées pour tous les composants, Angular démarre le cycle de résumé suivant, mais au lieu d'effectuer des opérations, il compare les valeurs actuelles avec celles dont il se souvient du cycle de résumé précédent.

Les opérations suivantes sont en cours de vérification lors du cycle de digestion:

vérifiez que les valeurs transmises aux composants enfants sont les mêmes que les valeurs qui seraient utilisées pour mettre à jour les propriétés de ces composants maintenant.

vérifiez que les valeurs utilisées pour mettre à jour les éléments DOM sont les mêmes que les valeurs qui seraient utilisées pour mettre à jour ces éléments maintenant fonctionnent de la même manière.

vérifie tous les composants enfants

Et ainsi, l'erreur est générée lorsque les valeurs comparées sont différentes. , le blogueur Max Koretskyi a déclaré:

Le coupable est toujours le composant enfant ou une directive.

Et enfin, voici quelques exemples du monde réel qui provoquent généralement cette erreur:

  • Services partagés
  • Diffusion d'événements synchrones
  • Instanciation dynamique des composants

Chaque échantillon peut être trouvé ici (plunkr), dans mon cas, le problème était une instanciation de composant dynamique.

De plus, par ma propre expérience, je recommande fortement à tout le monde d'éviter la setTimeoutsolution, dans mon cas a provoqué une boucle "presque" infinie (21 appels dont je ne veux pas vous montrer comment les provoquer),

Je recommanderais de toujours garder à l'esprit le cycle de vie angulaire afin que vous puissiez prendre en compte la façon dont ils seraient affectés chaque fois que vous modifiez la valeur d'un autre composant. Avec cette erreur Angular vous dit:

Vous faites peut-être mal, êtes-vous sûr d'avoir raison?

Le même blog dit également:

Souvent, le correctif consiste à utiliser le bon crochet de détection de changement pour créer un composant dynamique

Un petit guide pour moi est de considérer au moins les deux choses suivantes lors du codage ( je vais essayer de le compléter au fil du temps ):

  1. Évitez de modifier les valeurs de composant parent à partir des composants de son enfant, à la place: modifiez-les à partir de son parent.
  2. Lorsque vous utilisez les directives @Inputet, @Outputessayez d'éviter de déclencher des modifications de lyfecycle à moins que le composant ne soit complètement initialisé.
  3. Évitez les appels inutiles this.cdr.detectChanges();car ils peuvent déclencher plus d'erreurs, en particulier lorsque vous traitez avec beaucoup de données dynamiques
  4. Lorsque l'utilisation de this.cdr.detectChanges();est obligatoire, assurez-vous que les variables ( @Input, @Output, etc) utilisées sont remplies / initialisées au crochet de détection droit ( OnInit, OnChanges, AfterView, etc)
  5. Lorsque cela est possible, supprimez plutôt que cachez , ceci est lié aux points 3 et 4.

Aussi

Si vous voulez bien comprendre Angular Life Hook, je vous recommande de lire la documentation officielle ici:

Luis Limas
la source
Je ne comprends toujours pas pourquoi changer le ChangeDetectionStrategypour le OnPushréparer pour moi. J'ai un composant simple, qui a [disabled]="isLastPage()". Cette méthode lit le MatPaginator-ViewChild et retourne this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Le paginateur n'est pas disponible tout de suite, mais après avoir été lié à l'aide de @ViewChild. Changer l' ChangeDetectionStrategyerreur a supprimé l'erreur - et la fonctionnalité existe toujours comme avant. Je ne sais pas quels inconvénients j'ai maintenant, mais merci!
Igor
C'est génial @Igor! le seul inconvénient de l'utilisation OnPushest que vous devrez utiliser this.cdr.detectChanges()chaque fois que vous souhaitez que votre composant soit actualisé. Je suppose que vous l'utilisiez déjà
Luis Limas
9

Dans notre cas, nous FIXE en ajoutant changeDetection dans le composant et appelons detectChanges () dans ngAfterContentChecked, code comme suit

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

}
Charitha Goonewardena
la source
2

Je pense que la solution la meilleure et la plus propre que vous puissiez imaginer est la suivante:

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

Testé avec Angular 5.2.9

JavierFuentes
la source
3
C'est piraté ... et tellement inutile.
JoeriShoeby
1
@JoeriShoeby toutes les autres solutions ci-dessus sont des solutions de contournement basées sur setTimeouts ou des événements angulaires avancés ... cette solution est pure ES2017 prise en charge par tous les principaux navigateurs developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search =
await
1
Que faire si vous créez une application mobile (ciblant l'API Android 17) en utilisant Angular + Cordova par exemple, où vous ne pouvez pas compter sur les fonctionnalités d'ES2017? Notez que la réponse acceptée est une solution, pas une solution de contournement.
JoeriShoeby
@JoeriShoeby À l'aide de typescript, il est compilé en JS, donc aucun problème de prise en charge des fonctionnalités.
biggbest le
@biggbest Que voulez-vous dire par là? Je sais que Typescript sera compilé en JS, mais cela ne vous permet pas de supposer que tout JS fonctionnera. Notez que Javascript est exécuté dans un navigateur Web et que chaque navigateur se comporte différemment.
JoeriShoeby
2

Un petit travail autour que j'ai utilisé plusieurs fois

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

Je remplace principalement "null" par une variable que j'utilise dans le contrôleur.

Khateeb321
la source
1

Bien qu'il y ait déjà beaucoup de réponses et un lien vers un très bon article sur la détection des changements, je voulais donner mes deux cents ici. Je pense que la vérification est là pour une raison, j'ai donc pensé à l'architecture de mon application et réalisé que les changements dans la vue peuvent être traités en utilisant BehaviourSubjectet le crochet de cycle de vie correct. Alors, voici ce que j'ai fait pour trouver une solution.

  • J'utilise un composant tiers ( fullcalendar) , mais j'utilise également Angular Material , donc bien que j'aie créé un nouveau plugin pour le style, obtenir l'apparence était un peu gênant car la personnalisation de l'en-tête du calendrier n'est pas possible sans bifurquer le repo et rouler le vôtre.
  • J'ai donc fini par obtenir la classe JavaScript sous-jacente et j'ai besoin d'initialiser mon propre en-tête de calendrier pour le composant. Cela nécessite le ViewChildrendu avant que mon parent ne soit rendu, ce qui n'est pas la façon dont Angular fonctionne. C'est pourquoi j'ai inclus la valeur dont j'ai besoin pour mon modèle dans un BehaviourSubject<View>(null):

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

Ensuite, lorsque je peux être sûr que la vue est vérifiée, je mets à jour ce sujet avec la valeur de @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);
  }

Ensuite, dans mon modèle, j'utilise simplement le asynctuyau. Pas de piratage avec détection des changements, pas d'erreurs, fonctionne sans problème.

N'hésitez pas à demander si vous avez besoin de plus de détails.

Thomi
la source
1

Utilisez une valeur de formulaire par défaut pour éviter l'erreur.

Au lieu d'utiliser la réponse acceptée consistant à appliquer detectChanges () dans ngAfterViewInit () (qui a également résolu l'erreur dans mon cas), j'ai plutôt décidé d'enregistrer une valeur par défaut pour un champ de formulaire dynamiquement requis, de sorte que lorsque le formulaire est mis à jour ultérieurement, sa validité n'est pas modifiée si l'utilisateur décide de modifier une option sur le formulaire qui déclencherait les nouveaux champs obligatoires (et entraînerait la désactivation du bouton d'envoi).

Cela a sauvé un tout petit peu de code dans mon composant et, dans mon cas, l'erreur a été complètement évitée.

T. Bulford
la source
C'est une très bonne pratique, avec cela, vous évitez un appel inutile àthis.cdr.detectChanges()
Luis Limas
1

J'ai eu cette erreur parce que j'ai déclaré une variable et que je voulais plus tard
changer sa valeur en utilisantngAfterViewInit

export class SomeComponent {

    header: string;

}

pour réparer que je suis passé de

ngAfterViewInit() { 

    // change variable value here...
}

à

ngAfterContentInit() {

    // change variable value here...
}
user12163165
la source