Come gestire l'eccezione "espressione è cambiata dopo che è stata controllata" Angular2 quando una proprietà del componente dipende dal datetime corrente


168

Il mio componente ha stili che dipendono dal datetime corrente. Nel mio componente ho la seguente funzione.

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

lightnessAmpviene calcolato dal datetime corrente. Il colore cambia se dtoDateè nelle ultime 24 ore.

L'errore esatto è il seguente:

L'espressione è cambiata dopo essere stata controllata. Valore precedente: 'hsl (123, 80%, 49%)'. Valore attuale: 'hsl (123, 80%, 48%)'

So che l'eccezione appare in modalità di sviluppo solo al momento del controllo del valore. Se il valore selezionato è diverso dal valore aggiornato, viene generata l'eccezione.

Quindi ho cercato di aggiornare il datetime corrente ad ogni ciclo di vita nel seguente metodo hook per evitare l'eccezione:

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

... ma senza successo.


Risposte:


357

Esegui il rilevamento delle modifiche in modo esplicito dopo la modifica:

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

Soluzione perfetta, grazie. Ho notato che funziona anche con i seguenti metodi hook: ngOnChanges, ngDoCheck, ngAfterContentChecked. Quindi c'è un'opzione migliore?
Anthony Brenelière,

27
Dipende dal tuo caso d'uso. Se vuoi fare qualcosa quando un componente è inizializzato, di ngOnInit()solito è il primo posto. Se il codice dipende dal rendering del DOM ngAfterViewInit()o ngAfterContentInit()sono le opzioni successive. ngOnChanges()è adatto se il codice deve essere eseguito ogni volta che viene modificato un input. ngDoCheck()è per il rilevamento delle modifiche personalizzate. In realtà non so a cosa ngAfterViewChecked()serva meglio. Penso che si chiami poco prima o dopo ngAfterViewInit().
Günter Zöchbauer,

2
@KushalJayswal mi dispiace, non ha senso dalla tua descrizione. Suggerirei di creare una nuova domanda con il codice che dimostra ciò che si tenta di realizzare. Idealmente con un esempio StackBlitz.
Günter Zöchbauer,

4
Questa è anche un'ottima soluzione se lo stato del componente si basa su proprietà DOM calcolate dal browser, come clientWidth, ecc.
Giona

1
Appena usato su ngAfterViewInit, ha funzionato come un fascino.
Francisco Arleo,

42

TL; DR

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

Anche se questa è una soluzione alternativa, a volte è davvero difficile risolvere questo problema in modo più piacevole, quindi non incolpare te stesso se stai usando questo approccio. Va bene.

Esempi : il problema iniziale [ link ], risolto con setTimeout()[ link ]


Come evitare

In genere questo errore si verifica in genere dopo l'aggiunta da qualche parte (anche nei componenti padre / figlio) ngAfterViewInit. Quindi la prima domanda è porsi: posso vivere senza ngAfterViewInit? Forse sposti il ​​codice da qualche parte ( ngAfterViewCheckedpotrebbe essere un'alternativa).

Esempio : [ collegamento ]


Anche

Anche cose asincrone ngAfterViewInitche influenzano DOM potrebbero causare questo. Inoltre può essere risolto tramite setTimeouto aggiungendo l' delay(0)operatore nel tubo:

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

Esempio : [ collegamento ]


Buona lettura

Buon articolo su come eseguire il debug di questo e perché succede: link


2
Sembra essere più lento della soluzione selezionata
Pete B,

6
Questa non è la soluzione migliore, ma accidenti funziona sempre. La risposta selezionata non sempre funziona (è necessario capire abbastanza bene il gancio per farlo funzionare)
Freddy Bonda

Qual è la spiegazione corretta del perché questo funziona, qualcuno lo sa? È perché viene quindi eseguito in un thread diverso (asincrono)?
Knnhcn,

3
@knnhcn Non esiste un thread diverso in javascript. JS è a thread singolo per natura. SetTimeout dice al motore di eseguire la funzione qualche tempo dopo la scadenza del timer. Qui il timer è 0, che in realtà è trattato come 4 nei browser moderni, che è un sacco di tempo per angolare per fare le sue cose magiche: developer.mozilla.org/en-US/docs/Web/API/…
Kilves

27

Come menzionato da @leocaseiro sulla questione di github .

Ho trovato 3 soluzioni per coloro che sono alla ricerca di soluzioni facili.

1) Passaggio da ngAfterViewInitangAfterContentInit

2) Passaggio a ngAfterViewCheckedcombinato con ChangeDetectorRefcome suggerito su # 14748 (commento)

3) Continua con ngOnInit () ma chiama ChangeDetectorRef.detectChanges()dopo le modifiche.


1
Qualcuno può documentare qual è la soluzione più consigliata?
Pipo,

13

Lei vai due soluzioni!

1. Modifica ChangeDetectionStrategy su OnPush

Per questa soluzione, in pratica dirai angolare:

Smettere di controllare le modifiche; lo farò solo quando so che è necessario

La soluzione rapida:

Modifica il componente in modo che possa essere utilizzato ChangeDetectionStrategy.OnPush

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

Con questo, le cose sembrano non funzionare più. Questo perché d'ora in poi dovrai fare in modo che Angular chiami detectChanges()manualmente.

this.cdr.detectChanges();

Ecco un link che mi ha aiutato a capire bene ChangeDetectionStrategy : https://alligator.io/angular/change-detection-strategy/

2. Comprensione di ExpressionChangedAfterItHasBeenCheckedError

Ecco un piccolo estratto dalla risposta di tomonari_t sulle cause di questo errore, ho provato a includere solo le parti che mi hanno aiutato a capirlo.

L'articolo completo mostra esempi di codice reali su ogni punto mostrato qui.

La causa principale è il ciclo di vita angolare:

Dopo ogni operazione Angolare ricorda quali valori ha usato per eseguire un'operazione. Sono memorizzati nella proprietà oldValues ​​della vista componenti.

Dopo aver eseguito i controlli per tutti i componenti Angolare avvia quindi il successivo ciclo di digest ma invece di eseguire le operazioni confronta i valori correnti con quelli che ricorda del precedente ciclo di digest.

Le seguenti operazioni sono in fase di controllo presso il ciclo digest:

verificare che i valori passati ai componenti figlio siano gli stessi dei valori che ora verrebbero utilizzati per aggiornare le proprietà di questi componenti.

verifica che i valori utilizzati per aggiornare gli elementi DOM siano gli stessi dei valori che verrebbero utilizzati per aggiornare questi elementi ora eseguono lo stesso.

controlla tutti i componenti figlio

Quindi, l'errore viene generato quando i valori confrontati sono diversi. , il blogger Max Koretskyi ha dichiarato:

Il colpevole è sempre il componente figlio o una direttiva.

E finalmente ecco alcuni esempi del mondo reale che di solito causano questo errore:

  • Servizi condivisi
  • Trasmissione di eventi sincroni
  • Istanziazione dinamica dei componenti

Ogni campione può essere trovato qui (plunkr), nel mio caso il problema era un'istanza di componente dinamica.

Inoltre, per mia esperienza, consiglio vivamente a tutti di evitare la setTimeoutsoluzione, nel mio caso ha causato un ciclo "quasi" infinito (21 chiamate che non sono disposto a mostrarti come provocarle),

Consiglio di tenere sempre presente il ciclo di vita angolare in modo da poter tenere conto di come sarebbero interessati ogni volta che si modifica il valore di un altro componente. Con questo errore Angular ti sta dicendo:

Forse lo stai facendo nel modo sbagliato, sei sicuro di avere ragione?

Lo stesso blog dice anche:

Spesso, la soluzione consiste nell'utilizzare il giusto hook di rilevamento delle modifiche per creare un componente dinamico

Una breve guida per me è quella di considerare almeno le seguenti due cose durante la codifica ( cercherò di completarlo nel tempo ):

  1. Evita invece di modificare i valori dei componenti principali dai suoi componenti figlio, invece: modificali dai suoi genitori.
  2. Quando si utilizzano @Inpute le @Outputdirettive si tenta di evitare di attivare le modifiche del ciclo di vita a meno che il componente non sia completamente inizializzato.
  3. Evita chiamate inutili this.cdr.detectChanges();perché possono innescare più errori, specialmente quando hai a che fare con molti dati dinamici
  4. Quando l'uso di this.cdr.detectChanges();è obbligatorio, assicurarsi che le variabili ( @Input, @Output, etc) utilizzate siano riempite / inizializzate al gancio di rilevamento destro ( OnInit, OnChanges, AfterView, etc)
  5. Se possibile, rimuovere anziché nascondere , ciò è correlato ai punti 3 e 4.

Anche

Se vuoi comprendere appieno Angular Life Hook ti consiglio di leggere la documentazione ufficiale qui:


Non riesco ancora a capire perché cambiarlo ChangeDetectionStrategyper OnPushrisolverlo per me. Ho un componente semplice, che ha [disabled]="isLastPage()". Quel metodo sta leggendo MatPaginator-ViewChild e ritornando this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Il paginatore non è disponibile subito, ma dopo che è stato associato con @ViewChild. Cambiando il ChangeDetectionStrategyrimosso l'errore - e la funzionalità esiste ancora come prima. Non sono sicuro di quali inconvenienti ho ora, ma grazie!
Igor,

Ottimo @Igor! l'unico ritiro dell'utilizzo OnPushè che dovrai utilizzarlo this.cdr.detectChanges()ogni volta che desideri aggiornare il componente. Immagino che lo stavi già usando
Luis Limas il

9

Nel nostro caso abbiamo FISSO aggiungendo changeDetection nel componente e chiamando detectChanges () in ngAfterContentChecked, codice come segue

@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

Penso che la soluzione migliore e più pulita che tu possa immaginare sia questa:

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

Testato con Angolare 5.2.9


3
Questo è hacky ... e quindi inutile.
JoeriShoeby

1
@JoeriShoeby tutte le altre soluzioni di cui sopra sono soluzioni alternative basate su setTimeouts o eventi angolari avanzati ... questa soluzione è ES2017 pura supportata da tutti i maggiori browser developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search = await
JavierFuentes

1
Che cosa succede se stai costruendo un'applicazione mobile (targeting per API 17 Android) utilizzando Angular + Cordova, ad esempio, dove non puoi fare affidamento sulle funzionalità ES2017? Nota che la risposta accettata è una soluzione, non una soluzione
alternativa

@JoeriShoeby Usando il dattiloscritto, è compilato in JS, quindi nessun problema con il supporto delle funzionalità.
biggbest,

@biggbest Cosa intendi con questo? So che Typescript sarà compilato su JS, ma ciò non ti rende in grado di supporre che tutto JS funzionerà. Si noti che Javascript viene eseguito in un browser Web e ogni browser si comporta diversamente su di esso.
JoeriShoeby,

2

Un piccolo lavoro intorno l'ho usato molte volte

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

Sostituisco principalmente "null" con alcune variabili che uso nel controller.


1

Sebbene ci siano già molte risposte e un link a un ottimo articolo sul rilevamento delle modifiche, ho voluto dare qui i miei due centesimi. Penso che il controllo sia lì per un motivo, quindi ho pensato all'architettura della mia app e ho capito che le modifiche alla vista possono essere gestite utilizzando BehaviourSubjecte il gancio del ciclo di vita corretto. Quindi, ecco cosa ho fatto per una soluzione.

  • Uso un componente di terze parti ( fullcalendar) , ma utilizzo anche materiale angolare , quindi anche se ho creato un nuovo plug-in per lo stile, l'aspetto e l'aspetto sono stati un po 'imbarazzanti perché la personalizzazione dell'intestazione del calendario non è possibile senza il fork del repository e rotolare il tuo.
  • Quindi ho finito per ottenere la classe JavaScript sottostante e ho bisogno di inizializzare la mia intestazione del calendario per il componente. Ciò richiede ViewChildche sia reso prima che il mio genitore sia reso, il che non è il modo in cui funziona Angular. Ecco perché ho racchiuso il valore di cui ho bisogno per il mio modello in un BehaviourSubject<View>(null):

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

Successivamente, quando posso essere sicuro che la vista sia selezionata, aggiorno quell'oggetto con il valore di @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);
  }

Quindi, nel mio modello, utilizzo semplicemente il async pipe. Nessun hacking con rilevamento delle modifiche, nessun errore, funziona senza problemi.

Non esitate a chiedere se avete bisogno di maggiori dettagli.


1

Utilizzare un valore di modulo predefinito per evitare l'errore.

Invece di utilizzare la risposta accettata di applicare detectChanges () in ngAfterViewInit () (che ha anche risolto l'errore nel mio caso), ho deciso invece di salvare un valore predefinito per un campo modulo richiesto dinamicamente, in modo che quando il modulo viene successivamente aggiornato, la validità non viene modificata se l'utente decide di modificare un'opzione nel modulo che attiverà i nuovi campi obbligatori (e causerebbe la disabilitazione del pulsante di invio).

Ciò ha salvato un po 'di codice nel mio componente e nel mio caso l'errore è stato evitato del tutto.


Questa è davvero una buona pratica, con questo eviti una chiamata non necessaria athis.cdr.detectChanges()
Luis Limas

1

Ho avuto quell'errore perché ho dichiarato una variabile e in seguito ho voluto
modificarne il valore usandongAfterViewInit

export class SomeComponent {

    header: string;

}

per risolvere il problema da cui sono passato

ngAfterViewInit() { 

    // change variable value here...
}

per

ngAfterContentInit() {

    // change variable value here...
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.