Cómo gestionar Angular2 "la expresión ha cambiado después de haber sido verificada" cuando una propiedad de componente depende de la fecha y hora actual

168

Mi componente tiene estilos que dependen de la fecha y hora actual. En mi componente tengo la siguiente función.

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

lightnessAmpse calcula a partir de la fecha y hora actual. El color cambia si dtoDatees en las últimas 24 horas.

El error exacto es el siguiente:

La expresión ha cambiado después de que se verificó. Valor anterior: 'hsl (123, 80%, 49%)'. Valor actual: 'hsl (123, 80%, 48%)'

Sé que la excepción aparece en el modo de desarrollo solo en el momento en que se verifica el valor. Si el valor marcado es diferente del valor actualizado, se genera la excepción.

Así que intenté actualizar la fecha y hora actual en cada ciclo de vida en el siguiente método de enlace para evitar la excepción:

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

...pero sin éxito.

Anthony Brenelière
fuente

Respuestas:

357

Ejecute la detección de cambios explícitamente después del cambio:

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
fuente
Solución perfecta, gracias. Noté que también funciona con los siguientes métodos de enlace: ngOnChanges, ngDoCheck, ngAfterContentChecked. Entonces, ¿hay una mejor opción?
Anthony Brenelière
27
Eso depende de su caso de uso. Si desea hacer algo cuando se inicializa un componente, ngOnInit()generalmente es el primer lugar. Si el código depende de la representación del DOM ngAfterViewInit()o ngAfterContentInit()son las siguientes opciones. ngOnChanges()es una buena opción si el código debe ejecutarse cada vez que se modifica una entrada. ngDoCheck()es para la detección de cambios personalizados. En realidad no sé para qué ngAfterViewChecked()se usa mejor. Creo que se llama justo antes o después ngAfterViewInit().
Günter Zöchbauer
2
@KushalJayswal lo siento, no tiene sentido a partir de su descripción. Sugeriría crear una nueva pregunta con el código que demuestre lo que intenta lograr. Idealmente con un ejemplo de StackBlitz.
Günter Zöchbauer
44
Esta también es una excelente solución si el estado de su componente se basa en propiedades DOM calculadas por el navegador, como clientWidth, etc.
Jonah
1
Solo se usa en ngAfterViewInit, funcionó de maravilla.
Francisco Arleo
42

TL; DR

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

Aunque esta es una solución alternativa, a veces es realmente difícil resolver este problema de una manera más agradable, así que no se culpe si está utilizando este enfoque. Esta bien.

Ejemplos : El problema inicial [ enlace ], resuelto con setTimeout()[ enlace ]


Como evitar

En general, este error generalmente ocurre después de agregar en algún lugar (incluso en componentes principales / secundarios) ngAfterViewInit. Entonces, la primera pregunta es preguntarse: ¿puedo vivir sin él ngAfterViewInit? Quizás mueva el código a alguna parte ( ngAfterViewCheckedpodría ser una alternativa).

Ejemplo : [ enlace ]


también

También las cosas asíncronas ngAfterViewInitque afectan al DOM pueden causar esto. También se puede resolver mediante setTimeouto agregando el delay(0)operador en la tubería:

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

Ejemplo : [ enlace ]


Buena lectura

Buen artículo sobre cómo depurar esto y por qué sucede: enlace

Sergei Panfilov
fuente
2
Parece ser más lento que la solución seleccionada
Pete B
66
Esta no es la mejor solución, pero maldita sea, SIEMPRE funciona. La respuesta seleccionada no siempre funciona (uno necesita entender el gancho bastante bien para que funcione)
Freddy Bonda
¿Cuál es la explicación correcta de por qué esto funciona, alguien lo sabe? ¿Es porque luego se ejecuta en un hilo diferente (asíncrono)?
knnhcn
3
@knnhcn No existe un hilo diferente en javascript. JS es de un solo subproceso por naturaleza. SetTimeout solo le dice al motor que ejecute la función algún tiempo después de que expire el temporizador. Aquí el temporizador es 0, que en realidad se trata como 4 en los navegadores modernos, que es tiempo suficiente para que angular haga sus cosas mágicas: developer.mozilla.org/en-US/docs/Web/API/…
Kilves
27

Como mencionó @leocaseiro en el tema de github .

Encontré 3 soluciones para aquellos que buscan soluciones fáciles.

1) Pasar de ngAfterViewInitangAfterContentInit

2) Pasando a ngAfterViewCheckedcombinado con ChangeDetectorReflo sugerido en # 14748 (comentario)

3) Mantenga con ngOnInit () pero llame ChangeDetectorRef.detectChanges()después de sus cambios.

sincero
fuente
1
¿Alguien puede documentar cuál es la solución más recomendada?
Pipo
13

Ella te va dos soluciones!

1. Modificar ChangeDetectionStrategy a OnPush

Para esta solución, básicamente le estás diciendo a angular:

Dejar de buscar cambios; lo haré solo cuando sepa que es necesario

La solución rápida:

Modifique su componente para que use ChangeDetectionStrategy.OnPush

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

Con esto, las cosas ya no parecen funcionar. Eso es porque a partir de ahora tendrás que hacer que Angular llame detectChanges()manualmente.

this.cdr.detectChanges();

Aquí hay un enlace que me ayudó a entender ChangeDetectionStrategy a la derecha: https://alligator.io/angular/change-detection-strategy/

2. Comprender ExpressionChangedAfterItHasBeenCheckedError

Aquí hay un pequeño extracto de la respuesta de tomonari_t sobre las causas de este error. Intenté incluir solo las partes que me ayudaron a entender esto.

El artículo completo muestra ejemplos de código real sobre cada punto que se muestra aquí.

La causa raíz es el ciclo de vida angular:

Después de cada operación, Angular recuerda qué valores utilizó para realizar una operación. Se almacenan en la propiedad oldValues ​​de la vista de componentes.

Después de que se hayan realizado las comprobaciones de todos los componentes, Angular inicia el siguiente ciclo de resumen, pero en lugar de realizar operaciones, compara los valores actuales con los que recuerda del ciclo de resumen anterior.

Las siguientes operaciones se verifican en el ciclo de resumen:

compruebe que los valores transmitidos a los componentes secundarios son los mismos que los valores que se usarían para actualizar las propiedades de estos componentes ahora.

compruebe que los valores utilizados para actualizar los elementos DOM son los mismos que los valores que se usarían para actualizar estos elementos ahora realizan lo mismo.

comprueba todos los componentes secundarios

Y así, el error se produce cuando los valores comparados son diferentes. , el blogger Max Koretskyi declaró:

El culpable es siempre el componente hijo o una directiva.

Y finalmente, aquí hay algunas muestras del mundo real que generalmente causan este error:

  • Servicios compartidos
  • Difusión de eventos síncronos
  • Instanciación dinámica de componentes

Cada muestra se puede encontrar aquí (plunkr), en mi caso el problema era una instanciación dinámica de componentes.

Además, según mi propia experiencia, recomiendo encarecidamente a todos que eviten la setTimeoutsolución, en mi caso causó un bucle "casi" infinito (21 llamadas que no estoy dispuesto a mostrarle cómo provocarlas),

Recomendaría tener siempre en cuenta el ciclo de vida angular para que pueda tener en cuenta cómo se verían afectados cada vez que modifique el valor de otro componente. Con este error, Angular te dice:

Tal vez estás haciendo esto de la manera incorrecta, ¿estás seguro de que tienes razón?

El mismo blog también dice:

A menudo, la solución es usar el gancho de detección de cambio correcto para crear un componente dinámico

Una breve guía para mí es considerar al menos las siguientes dos cosas al codificar ( intentaré complementarlo con el tiempo ):

  1. Evite modificar los valores de los componentes primarios a partir de los componentes de su hijo, en su lugar: modifíquelos desde su padre.
  2. Cuando utilice @Inputy las @Outputdirectivas, trate de evitar desencadenar cambios en lyfecycle a menos que el componente esté completamente inicializado.
  3. Evite llamadas innecesarias, ya this.cdr.detectChanges();que pueden provocar más errores, especialmente cuando se trata de una gran cantidad de datos dinámicos.
  4. Cuando el uso de this.cdr.detectChanges();es obligatorio, asegúrese de que las variables ( @Input, @Output, etc) que se utilizan estén llenas / inicializadas en el gancho de detección correcto ( OnInit, OnChanges, AfterView, etc)
  5. Cuando sea posible, eliminar en lugar de ocultar , esto está relacionado con los puntos 3 y 4.

también

Si desea comprender completamente Angular Life Hook, le recomiendo que lea la documentación oficial aquí:

Luis Limas
fuente
Todavía no puedo entender por qué cambiarlo ChangeDetectionStrategypara OnPusharreglarlo por mí. Tengo un componente simple, que tiene [disabled]="isLastPage()". Ese método es leer el MatPaginator-ViewChild y regresar this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. El paginador no está disponible de inmediato, pero después de que se haya vinculado usando @ViewChild. Al cambiar, se ChangeDetectionStrategyeliminó el error, y la funcionalidad aún existe como antes. No estoy seguro de qué inconvenientes tengo ahora, ¡pero gracias!
Igor
¡Eso es genial @Igor! el único inconveniente OnPushes que tendrá que usarlo this.cdr.detectChanges()cada vez que desee que su componente se actualice. Supongo que ya lo estabas usando
Luis Limas
9

En nuestro caso, FIJAMOS agregando changeDetection en el componente y llama a detectChanges () en ngAfterContentChecked, codifique de la siguiente manera

@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
fuente
2

Creo que la mejor y más limpia solución que puedas imaginar es esta:

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

Probado con Angular 5.2.9

JavierFuentes
fuente
3
Esto es hacky ... y muy innecesario.
JoeriShoeby
1
@JoeriShoeby todas las demás soluciones anteriores son soluciones basadas en setTimeouts o eventos angulares avanzados ... esta solución es pura ES2017 compatible con todos los principales navegadores developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search =
await
1
¿Qué sucede si está creando una aplicación móvil (dirigida a Android API 17) usando Angular + Cordova, por ejemplo, donde no puede confiar en las funciones de ES2017? Tenga en cuenta que la respuesta aceptada es una solución, no una solución
alternativa
@JoeriShoeby Usando el mecanografiado, se compila a JS, por lo que no hay problemas de soporte de funciones.
biggbest
@biggbest ¿Qué quieres decir con eso? Sé que Typecript se compilará en JS, pero eso no te permite asumir que todo JS funcionará. Tenga en cuenta que Javascript se ejecuta en un navegador web, y cada navegador se comporta de manera diferente en él.
JoeriShoeby
2

Un pequeño trabajo que usé muchas veces

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

Principalmente reemplazo "nulo" con alguna variable que uso en el controlador.

Khateeb321
fuente
1

Aunque ya hay muchas respuestas y un enlace a un muy buen artículo sobre detección de cambios, quería dar mis dos centavos aquí. Creo que la verificación está ahí por una razón, así que pensé en la arquitectura de mi aplicación y me di cuenta de que los cambios en la vista pueden abordarse mediante el uso BehaviourSubjecty el enganche correcto del ciclo de vida. Entonces, esto es lo que hice por una solución.

  • Utilizo un componente de terceros ( fullcalendar) , pero también utilizo Angular Material , por lo que aunque hice un nuevo complemento para el estilo, obtener la apariencia fue un poco incómodo porque la personalización del encabezado del calendario no es posible sin bifurcar el repositorio y rodando el tuyo.
  • Así que terminé obteniendo la clase de JavaScript subyacente y necesito inicializar mi propio encabezado de calendario para el componente. Eso requiere ViewChildque se preste antes de que se preste mi padre, que no es la forma en que funciona Angular. Es por eso que envolví el valor que necesito para mi plantilla en BehaviourSubject<View>(null):

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

Luego, cuando puedo estar seguro de que la vista está marcada, actualizo ese tema con el valor 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);
  }

Luego, en mi plantilla, solo uso la asynctubería. Sin piratería con detección de cambios, sin errores, funciona sin problemas.

No dude en preguntar si necesita más detalles.

thomi
fuente
1

Use un valor de formulario predeterminado para evitar el error.

En lugar de usar la respuesta aceptada de aplicar detectChanges () en ngAfterViewInit () (que también resolvió el error en mi caso), decidí guardar un valor predeterminado para un campo de formulario requerido dinámicamente, de modo que cuando el formulario se actualice más tarde, su validez no cambia si el usuario decide cambiar una opción en el formulario que activaría los nuevos campos obligatorios (y provocaría que el botón de envío se deshabilitara).

Esto ahorró un poquito de código en mi componente, y en mi caso se evitó por completo el error.

T. Bulford
fuente
Esta es una muy buena práctica, con esto evitas una llamada innecesaria athis.cdr.detectChanges()
Luis Limas
1

Recibí ese error porque declaró una variable y luego quise
cambiar su valor usandongAfterViewInit

export class SomeComponent {

    header: string;

}

para arreglar que cambié de

ngAfterViewInit() { 

    // change variable value here...
}

a

ngAfterContentInit() {

    // change variable value here...
}
usuario12163165
fuente