La expresión ___ ha cambiado después de que se verificó

286

¿Por qué es el componente en este simple golpe?

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message:string = 'loading :(';

  ngAfterViewInit() {
    this.updateMessage();
  }

  updateMessage(){
    this.message = 'all done loading :)'
  }
}

lanzamiento:

EXCEPCIÓN: La expresión 'I'm {{message}} en la aplicación @ 0: 5' ha cambiado después de que se verificó. Valor anterior: 'Estoy cargando :('. Valor actual: 'Ya terminé de cargar :)' en [I'm {{message}} en App @ 0: 5]

cuando todo lo que estoy haciendo es actualizar un enlace simple cuando se inicia mi vista?

dibujó más
fuente
77
El artículo Todo lo que necesita saber sobre el ExpressionChangedAfterItHasBeenCheckedErrorerror explica el comportamiento con gran detalle.
Max Koretskyi
Considere modificar su ChangeDetectionStrategycuando use detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas
Solo piense que hay un control de entrada y que está llenando datos con un método y en el mismo método le está asignando algún valor. El compilador seguramente se confundirá con el valor nuevo / anterior. Por lo tanto, la encuadernación y la población deberían ocurrir de diferentes maneras.
MAC

Respuestas:

292

Como lo indicó drewmoore, la solución adecuada en este caso es activar manualmente la detección de cambios para el componente actual. Esto se realiza utilizando el detectChanges()método del ChangeDetectorRefobjeto (importado de angular2/core), o su markForCheck()método, que también actualiza los componentes principales. Ejemplo relevante :

import { Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App implements AfterViewInit {
  message: string = 'loading :(';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.message = 'all done loading :)'
    this.cdr.detectChanges();
  }

}

Aquí también hay Plunkers que demuestran los enfoques ngOnInit , setTimeout y enableProdMode por si acaso.

Kiara Grouwstra
fuente
77
En mi caso estaba abriendo un modal. Después de abrir el modal, estaba mostrando el mensaje "La expresión ___ ha cambiado después de que se verificó", por lo que mi solución se agregó this.cdr.detectChanges (); Después de abrir mi modal. ¡Gracias!
Jorge Casariego
¿Dónde está su declaración para la propiedad cdr? Esperaría ver una línea como cdr : anyo algo así en la messagedeclaración. ¿Solo me preocupa que me haya perdido algo?
CodeCabbie
1
@CodeCabbie está en los argumentos del constructor.
slasky
1
¡Esta resolución soluciona mi problema! ¡Muchas gracias! De manera muy clara y sencilla.
lwozniak
3
Esto me ayudó, con algunas modificaciones. Tuve que diseñar los elementos li que se generaron a través del bucle ngFor. Necesitaba cambiar el color de los tramos dentro de los elementos de la lista basados ​​en innerText al hacer clic en 'ordenar' que actualizaba un bool que se usaba como parámetro de una tubería de clasificación (el resultado ordenado era una copia de los datos, por lo que no se obtenían estilos actualizado con ngStyle solo). - En lugar de usar 'AfterViewInit', usé 'AfterViewChecked' - También me aseguré de importar e implementar AfterViewChecked. nota: configurar la tubería a 'puro: falso' no funcionaba, tuve que agregar este paso adicional (:
Chloe Corrigan el
170

Primero, tenga en cuenta que esta excepción solo se lanzará cuando esté ejecutando su aplicación en modo de desarrollo (que es el caso de forma predeterminada a partir de beta-0): si llama enableProdMode()al arrancar la aplicación, no se lanzará ( consulte plunk actualizado ).

En segundo lugar, no haga eso porque esta excepción se está lanzando por una buena razón: en resumen, cuando está en modo dev, cada ronda de detección de cambio es seguida inmediatamente por una segunda ronda que verifica que no hayan cambiado los enlaces desde el final de la primera, ya que esto indicaría que los cambios están siendo causados ​​por la detección de cambios en sí.

En su plunk, el enlace {{message}}se cambia por su llamada a setMessage(), que ocurre en el ngAfterViewInitgancho, que ocurre como parte del turno de detección de cambio inicial. Sin embargo, eso en sí mismo no es problemático: el problema es que setMessage()cambia el enlace pero no desencadena una nueva ronda de detección de cambios, lo que significa que este cambio no se detectará hasta que se desencadene una futura ronda de detección de cambios en otro lugar.

La conclusión: todo lo que cambie un enlace debe activar una ronda de detección de cambio cuando lo haga.

Actualice en respuesta a todas las solicitudes de un ejemplo de cómo hacerlo : la solución de @ Tycho funciona, al igual que los tres métodos en la respuesta que señaló @MarkRajcok. Pero, francamente, todos se sienten feos y equivocados para mí, como el tipo de trucos a los que nos acostumbramos en ng1.

Para estar seguro, hay ocasionales circunstancias en las que estos cortes son apropiadas, pero si usted está utilizando en cualquier cosa más que una muy forma ocasional, es una señal de que estás luchando contra el marco en lugar de abrazar plenamente su naturaleza reactiva.

En mi humilde opinión, una manera más idiomática, "Angular2" de abordar esto es algo así como: ( plunk )

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message | async}} </div>`
})
export class App {
  message:Subject<string> = new BehaviorSubject('loading :(');

  ngAfterViewInit() {
    this.message.next('all done loading :)')
  }
}
dibujó más
fuente
13
¿Por qué setMessage () no activa una nueva ronda de detección de cambios? Pensé que Angular 2 activaba automáticamente la detección de cambios cuando cambias el valor de algo en la interfaz de usuario.
Danny Libin
44
@drewmoore "Cualquier cosa que cambie un enlace necesita activar una ronda de detección de cambio cuando lo haga". ¿Cómo? ¿Es una buena práctica? ¿No debería completarse todo en una sola carrera?
Daniel Birowsky Popeski
2
@Tycho, de hecho. Desde que escribí ese comentario, he respondido otra pregunta donde describo las 3 formas de ejecutar la detección de cambios , que incluye detectChanges().
Mark Rajcok
44
solo para notar que, en el cuerpo de la pregunta actual, el método invocado se llama updateMessage, nosetMessage
superjos
3
@Daynil, tuve la misma sensación, hasta que leí el blog dado en el comentario bajo la pregunta: blog.angularindepth.com/… Explica por qué esto debe hacerse manualmente. En este caso, la detección de cambios de angular tiene un ciclo de vida. En caso de que algo cambie un valor entre estos ciclos de vida, entonces se debe ejecutar una detección de cambio enérgica (o un tiempo de espera establecido, que se ejecuta en el siguiente bucle de eventos, lo que activa la detección de cambio nuevamente).
Mahesh el
50

ngAfterViewChecked() trabajó para mi:

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

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}
Jaswanth Kumar
fuente
Como se mencionó anteriormente, el ciclo de detección de cambio del angular, que es de dos fases, tiene que detectar los cambios después de que se modifica la vista del niño, y creo que la mejor manera de hacerlo es usar el gancho del ciclo de vida proporcionado por el propio angular, y pedirle a Angular manualmente detectar el cambio y vincularlo. Mi opinión personal es que esta parece una respuesta apropiada.
Joey587
1
Esto funciona para mí, para cargar el componente dinámico de la jerarquía juntos.
Mehdi Daustany
47

Lo arreglé agregando ChangeDetectionStrategy desde el núcleo angular.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})
Biranchi
fuente
Esto funcionó para mí. Me pregunto cuál es la diferencia entre esto y usar ChangeDetectorRef
Ari
1
Hmm ... Entonces el modo del detector de cambio se establecerá inicialmente en CheckOnce( documentación )
Alex Klaus
Sí, el error / advertencia se ha ido. Pero está tomando mucho tiempo de carga que antes, como una diferencia de 5-7 segundos, que es enorme.
Kapil Raghuwanshi
1
@KapilRaghuwanshi Corre this.cdr.detectChanges();después de lo que sea que estés intentando cargar. Porque podría ser porque la detección de cambios no se está activando
Harshal Carpenter
36

¿No puedes usar ngOnInitporque solo estás cambiando la variable miembro message?

Si desea acceder a una referencia a un componente secundario @ViewChild(ChildComponent), debe esperar con él ngAfterViewInit.

Una solución sucia es llamar updateMessage()al siguiente bucle de eventos con, por ejemplo, setTimeout.

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}
Jo VdB
fuente
44
Cambiar mi código al método ngOnInit funcionó para mí.
Faliorn
29

Para esto, he intentado las respuestas anteriores, muchas no funcionan en la última versión de Angular (6 o posterior)

Estoy usando el control de material que requirió cambios después de realizar el primer enlace.

    export class AbcClass implements OnInit, AfterContentChecked{
        constructor(private ref: ChangeDetectorRef) {}
        ngOnInit(){
            // your tasks
        }
        ngAfterContentChecked() {
            this.ref.detectChanges();
        }
    }

Agregando mi respuesta, esto ayuda a algunos a resolver problemas específicos.

MarmiK
fuente
Esto realmente funcionó para mi caso, pero tiene un error tipográfico, después de implementar AfterContentChecked debería llamar a ngAfterContentChecked, no a ngAfterViewInit.
Tomas Lukac
Actualmente estoy en la versión 8.2.0 :)
Tomas Lukac
1
@ TomášLukáč Gracias por responder, he actualizado mi respuesta (y)
MarmiK
1
Recomiendo esta respuesta si ninguno de los anteriores no funciona.
Amir Choubani
afterContentChecked se llama cada vez que se vuelve a calcular o volver a comprobar el DOM. Intimando desde Angular versión 9
Imran Faruqi
24

El artículo Todo lo que necesita saber sobre el ExpressionChangedAfterItHasBeenCheckedErrorerror explica el comportamiento con gran detalle.

El problema con su configuración es que el ngAfterViewInitenlace del ciclo de vida se ejecuta después de las actualizaciones DOM procesadas de detección de cambios. Y está cambiando efectivamente la propiedad que se utiliza en la plantilla en este enlace, lo que significa que DOM debe volver a representarse:

  ngAfterViewInit() {
    this.message = 'all done loading :)'; // needs to be rendered the DOM
  }

y esto requerirá otro ciclo de detección de cambio y Angular por diseño solo ejecuta un ciclo de resumen.

Básicamente tienes dos alternativas para solucionarlo:

  • Actualice la propiedad asincrónicamente utilizando setTimeout, Promise.theno asincrónica observable referenciada en la plantilla

  • realice la actualización de la propiedad en un enlace antes de la actualización DOM: ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.

Max Koretskyi
fuente
Lea sus artículos: blog.angularindepth.com/… , Leerá otro blog.angularindepth.com/… pronto. Todavía no se pudo encontrar una solución para este problema. ¿Me puede decir qué sucede si agrego ngDoCheck o ngAfterContentChecked lifecycle hook y agrego esto this.cdr.markForCheck (); (cdr para ChangeDetectorRef) dentro de él. ¿No es una forma correcta de verificar los cambios después de que el ciclo de vida se enganche y se complete la verificación posterior?
Harsimer
9

Solo tiene que actualizar su mensaje en el enlace correcto del ciclo de vida, en este caso es en ngAfterContentCheckedlugar de ngAfterViewInit, porque en ngAfterViewInit se ha iniciado una comprobación del mensaje variable pero aún no ha finalizado.

ver: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview

entonces el código será solo:

import { Component } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message: string = 'loading :(';

  ngAfterContentChecked() {
     this.message = 'all done loading :)'
  }      
}

vea la demostración de trabajo en Plunker.

RedPelle
fuente
Estaba usando una combinación de un @ViewChildren()contador basado en la longitud que estaba poblado por un Observable. ¡Esta fue la única solución que funcionó para mí!
msanford
2
La combinación de ngAfterContentCheckedy ChangeDetectorRefdesde arriba funcionó para mí. En ngAfterContentCheckedllamada -this.cdr.detectChanges();
Kunal Dethe
7

Este error se produce porque el valor existente se actualiza inmediatamente después de la inicialización. Entonces, si actualiza el nuevo valor después de que el valor existente se representa en DOM, entonces funcionará bien. Como se menciona en este artículo Depuración angular "La expresión ha cambiado después de que se verificó"

por ejemplo puedes usar

ngOnInit() {
    setTimeout(() => {
      //code for your new value.
    });

}

o

ngAfterViewInit() {
  this.paginator.page
      .pipe(
          startWith(null),
          delay(0),
          tap(() => this.dataSource.loadLessons(...))
      ).subscribe();
}

Como puede ver, no he mencionado el tiempo en el método setTimeout. Como es la API proporcionada por el navegador, no una API de JavaScript, por lo tanto, esto se ejecutará por separado en la pila del navegador y esperará hasta que finalicen los elementos de la pila de llamadas.

Philip Roberts explica cómo el API evoca el concepto en uno de los videos de Youtube (¿Cuál es el truco del bucle de eventos?).

Sharad Jain
fuente
4

También puede poner su llamada a updateMessage () en el método ngOnInt (), al menos funciona para mí

ngOnInit() {
    this.updateMessage();
}

En RC1 esto no desencadena la excepción

Tobias Gassmann
fuente
3

También puede crear un temporizador utilizando la Observable.timerfunción rxjs y luego actualizar el mensaje en su suscripción:                    

Observable.timer(1).subscribe(()=> this.updateMessage());
rabinneslo
fuente
2

Lanza un error porque su código se actualiza cuando se llama a ngAfterViewInit () . Significa que su valor inicial cambió cuando se produce ngAfterViewInit. Si llama a eso en ngAfterContentInit () , no arrojará un error.

ngAfterContentInit() {
    this.updateMessage();
}
Sandip - Desarrollador Full Stack
fuente
0

No pude comentar sobre la publicación de @Biranchi ya que no tengo suficiente reputación, pero me solucionó el problema.

Una cosa a tener en cuenta! Si agregar changeDetection: ChangeDetectionStrategy.OnPush en el componente no funcionó, y es un componente hijo (componente tonto), intente agregarlo también al padre.

Esto solucionó el error, pero me pregunto cuáles son los efectos secundarios de esto.

TKDev
fuente
0

Obtuve un error similar al trabajar con datatable. Lo que sucede es que cuando usas * ngFor dentro de otra * ngFor datatable, arroja este error ya que interepta el ciclo de cambio angular. SO, en lugar de usar datatable dentro de datatable, use una tabla regular o reemplace mf.data con el nombre de la matriz. Esto funciona bien

Priyanka Arora
fuente
0

Creo que la solución más simple sería la siguiente:

  1. Realice una implementación de asignar un valor a alguna variable, es decir, a través de la función o setter.
  2. Cree una variable de clase (static working: boolean)en la clase donde existe esta función y cada vez que llame a la función, simplemente haga que sea verdadera lo que quiera. Dentro de la función, si el valor de trabajar es verdadero, simplemente regrese de inmediato sin hacer nada. De lo contrario, realice la tarea que desee. ¡Asegúrese de cambiar esta variable a falso una vez que se complete la tarea, es decir, al final de la línea de códigos o dentro del método de suscripción cuando haya terminado de asignar valores!
Imran Faruqi
fuente