Ошибка: ExpressionChangedAfterItHasBeenCheckedError, когда компонент Angular тестируется с синхронными Observables

У меня есть простой компонент, который использует службу, возвращающую Observable. Такой Observable используется для управления созданием html-элемента через *ngIf.

Здесь код

@Component({
  selector: 'hello',
  template: `<h1 *ngIf = "stuff$ | async as obj">Hello {{obj.name}}!</h1>`,
  styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements AfterViewInit {
  constructor(
    private myService: MyService,
  ) {}

  stuff$;

  ngAfterViewInit() {
    this.stuff$ = this.myService.getStuff();
  }
}


@Injectable({
  providedIn: 'root'
})
export class MyService {
  getStuff() {
    const stuff = {name: 'I am an object'};
    return of(stuff).pipe(delay(100));
  }
}

Как видите, getStuff() возвращает и Observable с delay, и все работает как положено.

Теперь я удаляю delay и получаю следующую ошибку на консоли

Ошибка: ExpressionChangedAfterItHasBeenCheckedError: выражение изменилось после проверки. Предыдущее значение: «ngIf: null». Текущее значение: «ngIf: [object Object]».

Это Stackbliz, который воспроизводит случай.

Это неизбежно? Есть ли способ избежать этой ошибки? Этот небольшой пример воспроизводит пример из реальной жизни, где MyService запрашивает серверную часть, поэтому задержка.

Это для меня актуально, потому что я хотел бы иметь возможность синхронно запускать тесты, имитирующие реальный бэкенд, и, когда я пробую такой подход, я получаю тот же результат.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
4
0
2 704
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Вы назначаете свой наблюдаемый после, инициализируя представление. Пытаться:

@Input() name: string;
stuff$ = this.myService.getStuff();

Или:

ngOnInit() { // or even in constructor()
    this.stuff$ = this.myService.getStuff();
}

Почему вы обрабатываете stuff$ как @Input, когда пытаетесь получить его и из myService? Это своего рода конфликт при рендеринге.

Возможно, ngAfterViewChecked будет работать лучше, чем ngAfterViewInit. Но решение по-прежнему не очень приятно.

Я тоже так думал, но посмотри внимательно. Это @Input() name: string;, а следующая строка — stuff$.

Harun Yilmaz 31.01.2019 15:37

Я удалил @Input() name, так как он не актуален.

Picci 31.01.2019 15:46
Ответ принят как подходящий

Ошибка в основном говорит о том, что одно изменение вызвало другое изменение. Это происходит только без delay(100), потому что RxJS строго последователен, а подписки происходят синхронно. Если вы используете delay(100) (даже если вы используете только delay(0), это делает эмиссию асинхронной, поэтому она происходит в другом кадре и перехватывается другим циклом обнаружения изменений.

Очень простой способ избежать этого — просто обернуть присвоение переменной с помощью setTimeout() или с помощью NgZone.run().

Или более «способ Rx» использует оператор subscribeOn(async):

import { asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';

...

return of(stuff).pipe(
  observeOn(asyncScheduler)
);

Если вам нужно синхронно изменить модель в обработчике жизненного цикла ngAfterViewInit, вы можете активировать обнаружение изменений, чтобы избежать ошибки ExpressionChangedAfterItHasBeenCheckedError:

constructor(private changeDetectorRef: ChangeDetectorRef) { }

ngAfterViewInit() {
  this.stuff$ = this.myService.getStuff();
  this.changeDetectorRef.detectChanges();
}

См. этот стекблиц для демонстрации.

Другие вопросы по теме