Como gerenciar a exceção "Expressão alterada após a verificação" do Angular2, quando uma propriedade do componente depende da data e hora atuais

168

Meu componente possui estilos que dependem da data e hora atuais. No meu componente, tenho a seguinte função.

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

lightnessAmpé calculado a partir da data e hora atuais. A cor muda se dtoDateestiver nas últimas 24 horas.

O erro exato é o seguinte:

A expressão foi alterada após a verificação. Valor anterior: 'hsl (123, 80%, 49%)'. Valor atual: 'hsl (123, 80%, 48%)'

Eu sei que a exceção aparece no modo de desenvolvimento apenas no momento em que o valor é verificado. Se o valor verificado for diferente do valor atualizado, a exceção será lançada.

Por isso, tentei atualizar a data e hora atual em cada ciclo de vida no seguinte método de gancho para evitar a exceção:

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

... mas sem sucesso.

Anthony Brenelière
fonte

Respostas:

357

Execute a detecção de alterações explicitamente após a alteração:

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
fonte
Solução perfeita, obrigado. Notei que ele também funciona com os seguintes métodos de gancho: ngOnChanges, ngDoCheck, ngAfterContentChecked. Então, existe uma melhor opção?
Anthony Brenelière 30/09/16
27
Isso depende do seu caso de uso. Se você deseja fazer algo quando um componente é inicializado, ngOnInit()geralmente é o primeiro lugar. Se o código depende do DOM que está sendo renderizado ngAfterViewInit()ou ngAfterContentInit()são as próximas opções. ngOnChanges()é um bom ajuste se o código for executado toda vez que uma entrada for alterada. ngDoCheck()é para detecção personalizada de alterações. Na verdade, não sei para que ngAfterViewChecked()serve melhor. Eu acho que é chamado antes ou depois ngAfterViewInit().
Günter Zöchbauer 30/09/16
2
@KushalJayswal desculpe, não pode fazer sentido com a sua descrição. Sugiro que você crie uma nova pergunta com o código que demonstra o que você tenta realizar. Idealmente com um exemplo StackBlitz.
Günter Zöchbauer
4
Essa também é uma excelente solução se o estado do componente for baseado em propriedades DOM calculadas pelo navegador, como clientWidthetc.
Jonah
1
Apenas usado no ngAfterViewInit, funcionou como um encanto.
Francisco Arleo 17/06
42

TL; DR

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

Embora essa seja uma solução alternativa, às vezes é realmente difícil resolver esse problema da melhor maneira possível; portanto, não se culpe se estiver usando essa abordagem. Tudo bem.

Exemplos : o problema inicial [ link ], resolvido com setTimeout()[ link ]


Como evitar

Em geral, esse erro geralmente ocorre após a adição de algum lugar (mesmo nos componentes pai / filho) ngAfterViewInit. Então, a primeira pergunta é se perguntar - posso viver sem ngAfterViewInit? Talvez você mova o código para algum lugar ( ngAfterViewCheckedpode ser uma alternativa).

Exemplo : [ link ]


Além disso

Também coisas assíncronas ngAfterViewInitque afetam o DOM podem causar isso. Também pode ser resolvido via setTimeoutou adicionando o delay(0)operador no tubo:

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

Exemplo : [ link ]


Nice Reading

Bom artigo sobre como depurar isso e por que isso acontece: link

Sergei Panfilov
fonte
2
Parece ser mais lento do que a solução escolhida
Pete B
6
Esta não é a melhor solução, mas droga, isso SEMPRE funciona. A resposta selecionada nem sempre funciona (é preciso entender o gancho muito bem fazê-lo funcionar)
Freddy Bonda
Qual é a explicação correta por que isso funciona, alguém sabe? É porque ele é executado em um Thread diferente (assíncrono)?
knnhcn
3
@knnhcn Não existe um tópico diferente em javascript. JS é de thread único por natureza. SetTimeout apenas diz ao mecanismo para executar a função algum tempo depois que o timer expirar. Aqui, o cronômetro é 0, que na verdade é tratado como 4 nos navegadores modernos, o que é bastante tempo para o angular fazer suas coisas mágicas: developer.mozilla.org/en-US/docs/Web/API/…
Kilves
27

Como mencionado por @leocaseiro na questão do github .

Encontrei 3 soluções para quem procura soluções fáceis.

1) Passando de ngAfterViewInitparangAfterContentInit

2) Movendo para ngAfterViewCheckedcombinado com ChangeDetectorRefconforme sugerido no # 14748 (comentário)

3) Continue com ngOnInit (), mas ligue ChangeDetectorRef.detectChanges()após as alterações.

candidJ
fonte
1
Alguém pode documentar qual é a solução mais recomendada?
Pipo
13

Ela você vai duas soluções!

1. Modifique ChangeDetectionStrategy para OnPush

Para esta solução, você está basicamente dizendo angular:

Pare de verificar se há alterações; farei isso apenas quando eu souber que é necessário

A solução rápida:

Modifique seu componente para que ele use ChangeDetectionStrategy.OnPush

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

Com isso, as coisas parecem não funcionar mais. Isso porque a partir de agora você terá que fazer a chamada Angular detectChanges()manualmente.

this.cdr.detectChanges();

Aqui está um link que me ajudou a entender o ChangeDetectionStrategy corretamente: https://alligator.io/angular/change-detection-strategy/

2. Entendendo ExpressionChangedAfterItHasBeenCheckedError

Aqui está um pequeno extrato da resposta tomonari_t sobre as causas desse erro. Tentei incluir apenas as partes que me ajudaram a entender isso.

O artigo completo mostra exemplos de códigos reais sobre todos os pontos mostrados aqui.

A causa raiz é o ciclo de vida angular:

Após cada operação, o Angular lembra quais valores foram usados ​​para executar uma operação. Eles são armazenados na propriedade oldValues ​​da visualização do componente.

Após a verificação de todos os componentes, o Angular inicia o próximo ciclo de resumo, mas em vez de executar operações, ele compara os valores atuais com os que se lembra do ciclo de resumo anterior.

As seguintes operações estão sendo verificadas nos ciclos de resumo:

verifique se os valores passados ​​para os componentes filhos são iguais aos valores que seriam usados ​​para atualizar as propriedades desses componentes agora.

verifique se os valores usados ​​para atualizar os elementos DOM são os mesmos que os valores que seriam usados ​​para atualizar esses elementos agora executam o mesmo.

verifica todos os componentes filhos

E assim, o erro é gerado quando os valores comparados são diferentes. , o blogueiro Max Koretskyi afirmou:

O culpado é sempre o componente filho ou uma diretiva.

E, finalmente, aqui estão algumas amostras do mundo real que geralmente causam esse erro:

  • Serviços compartilhados
  • Transmissão síncrona de eventos
  • Instanciação dinâmica de componentes

Cada amostra pode ser encontrada aqui (plunkr); no meu caso, o problema era uma instanciação de componente dinâmico.

Além disso, pela minha própria experiência, recomendo fortemente que todos evitem a setTimeoutsolução, no meu caso causou um loop infinito "quase" (21 chamadas que não estou disposto a mostrar como provocá-las),

Eu recomendaria ter sempre em mente os ciclos de vida angulares para que você possa levar em consideração como eles seriam afetados toda vez que você modificar o valor de outro componente. Com este erro, a Angular está lhe dizendo:

Talvez você esteja fazendo isso da maneira errada, tem certeza de que está certo?

O mesmo blog também diz:

Freqüentemente, a correção é usar o gancho de detecção de alterações correto para criar um componente dinâmico

Um pequeno guia para mim é considerar pelo menos as duas coisas a seguir durante a codificação ( tentarei complementá-lo com o tempo ):

  1. Evite modificar os valores dos componentes pai dos componentes da criança: em vez disso, modifique-os a partir do pai.
  2. Quando você usa @Inpute @Outputdirectivas tentar evitar desencadear mudanças lyfecycle a menos que o componente é completamente inicializado.
  3. Evite chamadas desnecessárias, this.cdr.detectChanges();pois elas podem acionar mais erros, especialmente quando você está lidando com muitos dados dinâmicos
  4. Quando o uso de this.cdr.detectChanges();for obrigatório, verifique se as variáveis ​​( @Input, @Output, etc) usadas estão preenchidas / inicializadas no gancho de detecção direito ( OnInit, OnChanges, AfterView, etc)
  5. Quando possível, remova em vez de ocultar , isso está relacionado aos pontos 3 e 4.

Além disso

Se você deseja entender completamente o Angular Life Hook, recomendo que você leia a documentação oficial aqui:

Luis Limas
fonte
Ainda não consigo entender por que alterar o ChangeDetectionStrategypara OnPushcorrigi-lo para mim. Eu tenho um componente simples, que tem [disabled]="isLastPage()". Esse método está lendo o MatPaginator-ViewChild e retornando this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. O paginador não está disponível imediatamente, mas depois de ter sido vinculado ao uso @ViewChild. Alterar o ChangeDetectionStrategyerro removido - e a funcionalidade ainda existe como antes. Não tenho certeza de quais inconvenientes tenho agora, mas obrigado!
Igor
Isso é ótimo, @Igor! a única retirada OnPushé que você precisará usar this.cdr.detectChanges()toda vez que quiser que seu componente seja atualizado. Eu acho que você já estava usando
Luis Limas
9

No nosso caso, corrigimos adicionando changeDetection no componente e chamamos detectChanges () em ngAfterContentChecked, codifique da seguinte maneira

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

Eu acho que a melhor e mais limpa solução que você pode imaginar é 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 }
    );
  }
}

Testado com Angular 5.2.9

JavierFuentes
fonte
3
Isso é hacky ... e tão desnecessário.
precisa saber é o seguinte
1
Todas as outras soluções acima são soluções alternativas baseadas em setTimeouts ou em eventos angulares avançados ... esta solução é pura ES2017, suportada por todos os principais navegadores developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search = aguardar
JavierFuentes
1
E se você estiver criando um aplicativo móvel (segmentando a API do Android 17) usando Angular + Cordova, por exemplo, onde não pode confiar nos recursos do ES2017? Observe a resposta aceita é uma solução, não um trabalho em torno ..
JoeriShoeby
@JoeriShoeby Usando texto datilografado, ele é compilado em JS, portanto, não há problemas de suporte a recursos.
Bigbest
@biggbest O que você quer dizer com isso? Eu sei que o Typescript será compilado para JS, mas isso não permite que você assuma que todo JS funcionará. Observe que o Javascript é executado em um navegador da Web e todos os navegadores se comportam de maneira diferente.
JoeriShoeby
2

Um pequeno trabalho que usei muitas vezes

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

Substituo principalmente "null" por alguma variável que uso no controlador.

Khateeb321
fonte
1

Embora já existam muitas respostas e um link para um artigo muito bom sobre detecção de alterações, eu queria dar meus dois centavos aqui. Acho que a verificação existe por um motivo, então pensei na arquitetura do meu aplicativo e percebi que as alterações na exibição podem ser tratadas usando BehaviourSubjecto gancho do ciclo de vida correto. Então aqui está o que eu fiz para uma solução.

  • Eu uso um componente de terceiros ( fullcalendar) , mas também uso o Angular Material , por isso, embora eu tenha criado um novo plug-in para estilizar, a aparência e a sensação foram um pouco estranhas, porque a personalização do cabeçalho da agenda não é possível sem a bifurcação do repositório e rolando o seu próprio.
  • Acabei recebendo a classe JavaScript subjacente e preciso inicializar meu próprio cabeçalho de calendário para o componente. Isso exige que o ViewChildarquivo seja renderizado antes que meu pai seja renderizado, e não é assim que o Angular funciona. É por isso que agrupei o valor necessário para o meu modelo em BehaviourSubject<View>(null):

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

Em seguida, quando posso ter certeza de que a exibição está marcada, atualizo esse assunto com o 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);
  }

Então, no meu modelo, eu apenas uso o asynccachimbo. Sem hackers com detecção de alterações, sem erros, funciona sem problemas.

Por favor, não hesite em perguntar se você precisa de mais detalhes.

thomi
fonte
1

Use um valor de formulário padrão para evitar o erro.

Em vez de usar a resposta aceita de aplicar detectChanges () em ngAfterViewInit () (que também resolveu o erro no meu caso), decidi salvar um valor padrão para um campo de formulário dinamicamente necessário, para que, quando o formulário for atualizado posteriormente, sua validade não será alterada se o usuário decidir alterar uma opção no formulário que acionaria os novos campos obrigatórios (e desativaria o botão de envio).

Isso economizou um pouquinho de código no meu componente e, no meu caso, o erro foi totalmente evitado.

T. Bulford
fonte
Esta é realmente uma boa prática, com isso você evita chamadas desnecessárias parathis.cdr.detectChanges()
Luis Limas
1

Eu recebi esse erro porque declarei uma variável e depois desejei
alterar seu valor usandongAfterViewInit

export class SomeComponent {

    header: string;

}

para corrigir que eu mudei de

ngAfterViewInit() { 

    // change variable value here...
}

para

ngAfterContentInit() {

    // change variable value here...
}
user12163165
fonte