Jak zarządzać wyjątkiem „wyrażenie zmieniło się po sprawdzeniu” w Angular2, gdy właściwość komponentu zależy od bieżącej daty i godziny

168

Mój komponent ma style zależne od bieżącej daty i godziny. W moim komponencie mam następującą funkcję.

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

lightnessAmpjest obliczany na podstawie bieżącej daty i godziny. Kolor zmienia się, jeśli dtoDatewystąpił w ciągu ostatnich 24 godzin.

Dokładny błąd jest następujący:

Wyrażenie zmieniło się po sprawdzeniu. Poprzednia wartość: „hsl (123, 80%, 49%)”. Aktualna wartość: „hsl (123, 80%, 48%)”

Wiem, że wyjątek pojawia się w trybie programistycznym tylko w momencie sprawdzania wartości. Jeśli zaznaczona wartość różni się od zaktualizowanej wartości, zostanie zgłoszony wyjątek.

Próbowałem więc zaktualizować bieżącą datę i godzinę w każdym cyklu życia w następujący sposób, aby zapobiec wyjątkowi:

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

... ale bez powodzenia.

Anthony Brenelière
źródło

Odpowiedzi:

357

Uruchom wykrywanie zmian jawnie po zmianie:

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
źródło
Idealne rozwiązanie, dziękuję. Zauważyłem, że działa również z następującymi metodami przechwytującymi: ngOnChanges, ngDoCheck, ngAfterContentChecked. Więc czy jest najlepsza opcja?
Anthony Brenelière
28
To zależy od twojego przypadku użycia. Jeśli chcesz coś zrobić, gdy komponent jest inicjowany, ngOnInit()zwykle jest to pierwsze miejsce. Jeśli kod zależy od renderowanego modelu DOM, ngAfterViewInit()czy ngAfterContentInit()są to następne opcje. ngOnChanges()jest dobrym rozwiązaniem, jeśli kod powinien być wykonywany za każdym razem, gdy dane wejściowe zostały zmienione. ngDoCheck()służy do niestandardowego wykrywania zmian. Właściwie nie wiem, do czego ngAfterViewChecked()najlepiej się nadaje. Myślę, że nazywa się to tuż przed lub po ngAfterViewInit().
Günter Zöchbauer
2
@KushalJayswal przepraszam, nie ma sensu z twojego opisu. Proponuję utworzyć nowe pytanie z kodem, który pokaże, co próbujesz osiągnąć. Idealnie z przykładem StackBlitz.
Günter Zöchbauer
4
Jest to również doskonałe rozwiązanie, jeśli stan komponentu jest oparty na właściwościach DOM obliczonych przez przeglądarkę, takich jak clientWidthitp.
Jonah
1
Po prostu użyty w ngAfterViewInit, działał jak urok.
Francisco Arleo
42

TL; DR

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

Chociaż jest to obejście, czasami naprawdę trudno jest rozwiązać ten problem w lepszy sposób, więc nie obwiniaj siebie, jeśli używasz tego podejścia. W porządku.

Przykłady : początkowy problem [ link ], rozwiązany za pomocą setTimeout()[ link ]


Jak ominąć

Ogólnie ten błąd występuje zwykle po dodaniu czegoś (nawet w komponentach nadrzędnych / podrzędnych) ngAfterViewInit. Więc pierwsze pytanie brzmi: czy mogę żyć bez ngAfterViewInit? Być może gdzieś przenosisz kod ( ngAfterViewCheckedmoże to być alternatywa).

Przykład : [ link ]


Również

Również rzeczy asynchroniczne, ngAfterViewInitktóre mają wpływ na DOM, mogą to powodować. Można to również rozwiązać za pomocą setTimeoutlub dodając delay(0)operator w potoku:

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

Przykład : [ link ]


Miłe czytanie

Dobry artykuł o tym, jak to debugować i dlaczego tak się dzieje: link

Siergiej Panfiłow
źródło
2
Wydaje się, że jest wolniejsze niż wybrane rozwiązanie
Pete B
6
To nie jest najlepsze rozwiązanie, ale do cholery działa ZAWSZE. Wybrana odpowiedź nie zawsze działa (trzeba dość dobrze zrozumieć haczyk, żeby zadziałał)
Freddy Bonda.
Jakie jest prawidłowe wyjaśnienie, dlaczego to działa, ktoś wie? Czy to dlatego, że działa wtedy w innym wątku (async)?
knnhcn
3
@knnhcn W javascript nie ma czegoś takiego jak inny wątek. JS jest z natury jednowątkowy. SetTimeout po prostu nakazuje silnikowi wykonanie funkcji jakiś czas po wygaśnięciu licznika czasu. Tutaj licznik czasu wynosi 0, co w nowoczesnych przeglądarkach jest traktowane jako 4, co daje mnóstwo czasu, aby Angular zrobił swoje magiczne rzeczy: developer.mozilla.org/en-US/docs/Web/API/ ...
Kilves,
27

Jak wspomniał @leocaseiro w sprawie github .

Znalazłem 3 rozwiązania dla tych, którzy szukają łatwych rozwiązań.

1) Przejście z ngAfterViewInitdongAfterContentInit

2) Przejście do ngAfterViewCheckedpołączonych z ChangeDetectorRefzgodnie z sugestią # 14748 (komentarz)

3) Kontynuuj z ngOnInit (), ale wywołaj ChangeDetectorRef.detectChanges()po wprowadzeniu zmian.

candidJ
źródło
1
Czy każdy może udokumentować, jakie jest najbardziej zalecane rozwiązanie?
Pipo
13

Jej masz dwa rozwiązania!

1. Zmodyfikuj ChangeDetectionStrategy na OnPush

W przypadku tego rozwiązania w zasadzie podasz kąt:

Przestań sprawdzać zmiany; zrobię to tylko wtedy, gdy wiem, że jest to konieczne

Szybka naprawa:

Zmodyfikuj swój komponent, aby używał ChangeDetectionStrategy.OnPush

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

Dzięki temu rzeczy już nie działają. To dlatego, że od teraz będziesz musiał detectChanges()ręcznie wywoływać Angular .

this.cdr.detectChanges();

Oto link, który pomógł mi dobrze zrozumieć ChangeDetectionStrategy : https://alligator.io/angular/change-detection-strategy/

2. Zrozumienie ExpressionChangedAfterItHasBeenCheckedError

Oto mały fragment odpowiedzi tomonari_t na temat przyczyn tego błędu, próbowałem zawrzeć tylko te części, które pomogły mi to zrozumieć.

Pełny artykuł zawiera prawdziwe przykłady kodu dotyczące każdego punktu pokazanego tutaj.

Główną przyczyną jest kątowy cykl życia:

Po każdej operacji Angular pamięta, jakich wartości użył do wykonania operacji. Są one przechowywane we właściwości oldValues ​​widoku komponentu.

Po sprawdzeniu wszystkich składników, Angular rozpoczyna następny cykl podsumowania, ale zamiast wykonywać operacje, porównuje bieżące wartości z wartościami, które pamięta z poprzedniego cyklu podsumowania.

Następujące operacje są sprawdzane w cyklu podsumowania:

sprawdź, czy wartości przekazywane do komponentów podrzędnych są takie same, jak wartości, które byłyby teraz używane do aktualizacji właściwości tych komponentów.

sprawdź, czy wartości używane do aktualizacji elementów DOM są takie same, jak wartości, które byłyby używane do aktualizacji tych elementów, teraz działają tak samo.

sprawdza wszystkie komponenty potomne

I tak błąd jest generowany, gdy porównywane wartości są różne. , bloger Max Koretskyi stwierdził:

Winowajcą jest zawsze składnik potomny lub dyrektywa.

I na koniec kilka przykładów z rzeczywistego świata, które zwykle powodują ten błąd:

  • Usługi wspólne
  • Synchroniczne nadawanie wydarzeń
  • Dynamiczne tworzenie instancji komponentów

Każdy przykład można znaleźć tutaj (plunkr), w moim przypadku problemem była dynamiczna instancja komponentu.

Również z własnego doświadczenia zdecydowanie polecam wszystkim unikanie setTimeoutrozwiązania, w moim przypadku spowodowało to "prawie" nieskończoną pętlę (21 wezwań, których nie jestem skłonny pokazać, jak je sprowokować),

Radziłbym zawsze pamiętać o cyklu życia Angulara, abyś mógł wziąć pod uwagę, jak wpłynie to na nie za każdym razem, gdy modyfikujesz wartość innego komponentu. Z tym błędem Angular mówi ci:

Może robisz to w niewłaściwy sposób, czy na pewno masz rację?

Ten sam blog mówi również:

Często rozwiązaniem jest użycie odpowiedniego zaczepu do wykrywania zmian w celu utworzenia komponentu dynamicznego

Krótkim przewodnikiem dla mnie jest rozważenie co najmniej dwóch następujących rzeczy podczas kodowania ( z czasem postaram się to uzupełnić ):

  1. Unikaj modyfikowania wartości komponentu nadrzędnego z jego komponentów podrzędnych, zamiast tego: modyfikuj je z jego rodzica.
  2. Podczas korzystania z dyrektyw @Inputi @Outputstaraj się unikać wyzwalania zmian w cyklu życia, chyba że składnik zostanie całkowicie zainicjowany.
  3. Unikaj niepotrzebnych wywołań, this.cdr.detectChanges();które mogą powodować więcej błędów, szczególnie gdy masz do czynienia z dużą ilością danych dynamicznych
  4. Gdy użycie this.cdr.detectChanges();jest obowiązkowe, upewnij się, że używane zmienne ( @Input, @Output, etc) są wypełnione / zainicjowane na prawym haku wykrywania ( OnInit, OnChanges, AfterView, etc)
  5. Jeśli to możliwe, raczej usuń niż ukryj , jest to związane z punktem 3 i 4.

Również

Jeśli chcesz w pełni zrozumieć Angular Life Hook, polecam przeczytanie oficjalnej dokumentacji tutaj:

Luis Limas
źródło
Wciąż nie mogę zrozumieć dlaczego zmieniając ChangeDetectionStrategyaby OnPushnaprawić go dla mnie. Mam prosty element, który ma [disabled]="isLastPage()". Ta metoda odczytuje MatPaginator-ViewChild i zwraca this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Paginator nie jest dostępny od razu, ale po powiązaniu z użyciem @ViewChild. Zmiana ChangeDetectionStrategyusuniętego błędu - a funkcjonalność nadal istnieje jak poprzednio. Nie jestem pewien, jakie mam teraz wady, ale dziękuję!
Igor
To świetnie @Igor! jedyną rezygnacją z używania OnPushjest to, że będziesz musiał używać this.cdr.detectChanges()za każdym razem, gdy chcesz, aby twój komponent był odświeżany. Myślę, że już go
używałeś
9

W naszym przypadku NAPRAWIONO przez dodanie changeDetection do komponentu i wywołanie funkcji detekcji zmian () w ngAfterContentChecked, kod w następujący sposób

@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
źródło
2

Myślę, że najlepszym i najczystszym rozwiązaniem, jakie możesz sobie wyobrazić, jest:

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

Testowane z Angular 5.2.9

JavierFuentes
źródło
3
To jest hacky ... i tak niepotrzebne.
JoeriShoeby
1
@JoeriShoeby wszystkie inne powyższe rozwiązania to obejścia oparte na setTimeouts lub zaawansowanych zdarzeniach Angular ... to rozwiązanie jest w pełni ES2017 obsługiwane przez wszystkie główne przeglądarki developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search =
await
1
A co, jeśli budujesz aplikację mobilną (ukierunkowaną na Android API 17), na przykład przy użyciu Angular + Cordova, gdzie nie możesz polegać na funkcjach ES2017? Zauważ, że zaakceptowana odpowiedź jest rozwiązaniem, a nie obejściem.
JoeriShoeby
@JoeriShoeby Używając skryptu, jest skompilowany do JS, więc nie ma problemów z obsługą funkcji.
biggbest
@biggbest Co masz na myśli? Wiem, że Typescript zostanie skompilowany do JS, ale nie oznacza to, że możesz założyć, że cały JS będzie działał. Zwróć uwagę, że JavaScript jest uruchamiany w przeglądarce internetowej i każda przeglądarka zachowuje się w niej inaczej.
JoeriShoeby
2

Mała praca, z której korzystałem wiele razy

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

Głównie zastępuję "null" jakąś zmienną, której używam w kontrolerze.

Khateeb321
źródło
1

Chociaż jest już wiele odpowiedzi i link do bardzo dobrego artykułu o wykrywaniu zmian, chciałem tutaj dać swoje dwa centy. Myślę, że to sprawdzenie ma swój powód, więc pomyślałem o architekturze mojej aplikacji i zdałem sobie sprawę, że zmiany w widoku można rozwiązać, używając BehaviourSubjecti prawidłowego haka cyklu życia. Oto, co zrobiłem dla rozwiązania.

  • Używam komponentu innej firmy ( pełny kalendarz) , ale używam również Angular Material , więc chociaż stworzyłem nową wtyczkę do stylizacji, uzyskanie wyglądu i stylu było nieco niezręczne, ponieważ dostosowanie nagłówka kalendarza nie jest możliwe bez rozwidlenia repozytorium i twórz własne.
  • Ostatecznie otrzymałem podstawową klasę JavaScript i muszę zainicjować własny nagłówek kalendarza dla komponentu. Wymaga ViewChildto renderowania przed renderowaniem mojego rodzica, co nie jest sposobem, w jaki działa Angular. Dlatego zawarłem wartość, której potrzebuję dla mojego szablonu w BehaviourSubject<View>(null):

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

Następnie, gdy mam pewność, że widok jest zaznaczony, aktualizuję ten temat o wartość z @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);
  }

Następnie w moim szablonie po prostu używam asyncrury. Bez hakowania z wykrywaniem zmian, bez błędów, działa płynnie.

Nie wahaj się zapytać, czy potrzebujesz więcej informacji.

thomi
źródło
1

Użyj domyślnej wartości formularza, aby uniknąć błędu.

Zamiast korzystać z akceptowanej odpowiedzi, jaką jest zastosowanie funkcji detekcji zmian () w ngAfterViewInit () (co również rozwiązało błąd w moim przypadku), zdecydowałem się zamiast tego zapisać wartość domyślną dla dynamicznie wymaganego pola formularza, tak aby przy późniejszej aktualizacji formularza, jego ważność nie jest zmieniana, jeśli użytkownik zdecyduje się zmienić opcję w formularzu, która spowoduje uruchomienie nowych wymaganych pól (i wyłączenie przycisku przesyłania).

Pozwoliło to zaoszczędzić trochę kodu w moim komponencie, aw moim przypadku całkowicie uniknięto błędu.

T. Bulford
źródło
To naprawdę dobra praktyka, dzięki której unikniesz niepotrzebnego telefonuthis.cdr.detectChanges()
Luis Limas
1

Otrzymałem ten błąd, ponieważ zadeklarowałem zmienną, a później chciałem
zmienić jej wartość za pomocąngAfterViewInit

export class SomeComponent {

    header: string;

}

naprawić to, z czego się zmieniłem

ngAfterViewInit() { 

    // change variable value here...
}

do

ngAfterContentInit() {

    // change variable value here...
}
user12163165
źródło