Как я могу исправить эту конкретную ошибку NG0100: ExpressionChangedAfterItHasBeenCheckedError?

Я читал о NG0100: ExpressionChangedAfterItHasBeenCheckedError в Angular, но не могу избежать этого в мое дело.

По сути, на перехватчике у меня есть служба, которая загружает «статус» true/false:

перехват (требуется: HttpRequest, следующий: HttpHandler): наблюдаемый { это.showLoader();

return next.handle(req).pipe(
  catchError((error) => {
    return error;
  }),
  finalize(() => {
    this.hideLoader();
  })
);

}

Использование ngAfterViewInit в компоненте приложения приводит к этой ошибке:

  ngOnInit(): void {}
  ngAfterViewInit() {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
  }

И мне нужно его использовать: на самом деле, когда все дочерние элементы загружены, родитель должен «отправить» (один раз) данные дочернему элементу (только в начале). И в какой-то момент мне просто нужно прочитать от ребенка (поэтому я использую ViewChild, а не механизм @Output).

Как я могу исправить эту конкретную ошибку? Должен ли я синхронизировать Observable? Не уверен, как...

Я бы предложил, чтобы отображение/скрытие для загрузчика обрабатывалось субъектом (который обновляется перехватчиком), который затем используется в компоненте с оператором debounceTime для предотвращения условий гонки. Вы можете установить значение всего 5 мс на debounceTime, чтобы ограничить выбросы без заметного влияния на производительность.

Will Alexander 23.04.2022 11:40

Ответ на это есть в ссылках из статьи, которую вы разместили в вопросе

Drenai 23.04.2022 12:20

@Drenai не совсем так. SetTimeout и Promise дают ту же ошибку. И не могу использовать ngOnInit, потому что дочерние еще не загружены в это время...

markzzz 23.04.2022 13:38

Может быть detectChanges - это тот, который они описывают, который я видел чаще всего

Drenai 23.04.2022 13:59

@Drenai кажется скорее «быстрым решением», чем решением :(

markzzz 23.04.2022 15:01
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Обработка ошибок при выполнении HTTP-запросов в JavaScript.
Обработка ошибок при выполнении HTTP-запросов в JavaScript.
Каждый проект должен выполнять HTTP-запросы, и, конечно, некоторые из этих запросов могут содержать ошибки. Нам нужно знать, как обрабатывать эти...
Включение UTF-8 в jsPDF с помощью Angular
Включение UTF-8 в jsPDF с помощью Angular
Привет, разработчики, я предполагаю, что вы уже знаете, как экспортировать pdf через jsPDF. Если ответ отрицательный, то вы можете ознакомиться с моей...
0
5
170
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

private _myChild: MyChildComponent;
@ViewChild('childSelector')
set childSelector(val: MyChildComponent) {
  this._myChild = val;
  this.getData().subscribe((data) => {
    this._myChild.loadRecipeRoadmap(data.name);
  });
}

вы также можете использовать набор свойств вместо afterInit. это не очень чисто, но это способ справиться с этим, особенно если он вложен с ngIfs или подобным.

редактировать: здесь, вероятно, лучший подход, чем хакерский метод выше.

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

@Injectable({
  providedIn: 'root',
})
export class LoaderService {
  private loaderSource = new BehaviorSubject<boolean>(false);

  constructor() {}

  show() {
    this.loaderSource.next(true);
  }
  hide() {
    this.loaderSource.next(false);
  }
  getLoader() {
    return this.loaderSource.asObservable();
  }
}

пусть это будет наблюдаемое, которое вы возвращаете из службы. Затем установите его в afterViewInit

ngAfterViewInit() {
  this.loader$ = this.loader.getLoader();
  this.getData().subscribe((data) => {
    this.childSelector.loadRecipeRoadmap(data.name);
  });
}

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

<app-my-child #childSelector></app-my-child>

<div *ngIf="loader$ | async">Loading...</div>

стекблиц https://stackblitz.com/edit/angular-ivy-sfmfrj?file=src%2Fapp%2Fapp.component.ts

На самом деле не получить ваше решение. Где/когда я должен использовать сеттер? В AfterInit? Ммм, вы можете привести мне реальный пример?

markzzz 23.04.2022 13:39

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

markzzz 23.04.2022 15:00

@markzzz Я обновил свой ответ другим подходом, который немного проще, но должен решить проблему.

Henrik Bøgelund Lavstsen 24.04.2022 08:09

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

markzzz 26.04.2022 09:45

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

Henrik Bøgelund Lavstsen 26.04.2022 15:11

Компонент не должен заботиться о функциональности других сервисов. Я не хочу вставлять дополнительное поведение, а не один компонент, для которого создан, извините.

markzzz 26.04.2022 16:23
Ответ принят как подходящий

Это потому, что вы выполняете некоторый код в ngAfterViewInit, который изменяет отображаемые данные..

Первый цикл обнаружения оценивал ngIf до false, затем выполнялся ngAfterViewInit, затем после него выполнялся второй проверочный цикл обнаружения (режим угловой разработки имеет этот дополнительный), и на этот раз ngIf оценивался как true. Отсюда и печально известная ошибка.

Есть несколько решений для этого:

вариант 1: следующая макрозадача (setTimeout)

Поскольку выборка ваших данных в любом случае асинхронна, вы можете отложить ее вызов в следующей макрозадаче (после завершения ngAfterViewInit) с помощью setTimeout с задержкой 0:

ngAfterViewInit() {
  setTimeout(() => {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
  });
}
вариант 2: ChangeDetectorRef.detectChanges()

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

constructor(private http: HttpClient, public loader: LoaderService, private changeDetectorRef: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
    this.changeDetectorRef.detectChanges();
  }

Или, может быть, более угловатый способ:

вариант 3: получение данных в OnInit + привязка данных

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

export class AppComponent {
  dataForChild: any;

  constructor(private http: HttpClient, public loader: LoaderService) {}

  ngOnInit(): void {
    this.getData().subscribe((data) => this.dataForChild = data.name);
  }

И свяжите его с (или, конечно, myData в дочернем компоненте нужно стать и ввести свойство с @Input())

<app-my-child [myData]="dataForChild"></app-my-child>

Дополнительно - ожидающая подписка

Вы подписываетесь на наблюдаемое, но не отписываетесь от него (ни с помощью unsubribe(), ни с помощью takeUntil()). Хотя подписки на наблюдаемые из модуля angular http не вызывают утечек памяти, хорошо убедиться, что вы все равно отмените подписку по нескольким причинам:

  • просто потому, что это хорошая практика, не задумываясь, нужно это или нет. Сегодня это может быть не нужно, но завтра кто-то изменит сервис, и он может стать небезопасным для наблюдения;
  • Если вы отмените подписку в ngOnDestroy, ожидающий HTTP-запрос будет отменен, что освободит пул используемых браузером запросов.
  • также никакой логики в подписке и в потенциальных преобразованиях pipe не будет вызываться, поэтому лучшая производительность за счет того, что не выполняется ненужный код
  • и это позволит сборщику мусора быстрее удалить экземпляр компонента

Превосходно. Вы очень хорошо раскрываете проблему, с множеством решений :) Принято. Не могли бы вы помочь мне и в этом? stackoverflow.com/questions/71952796/…

markzzz 27.04.2022 11:46

Пожалуйста, избегайте решения setTimeout, это плохая практика, поскольку она работает «вне» жизненного цикла Angular, в некоторых случаях она может даже вызвать дополнительные изменения обнаружения с риском создания чего-то очень похожего на циклические вызовы. Также избегайте варианта 2, если нет абсолютной необходимости вызывать руководство detectChanges (потому что он запускает новый жизненный цикл). Вариант 3 — лучший подход, поскольку он идеально сочетается с Angular Lifecycle.

luiscla27 28.04.2022 02:24

Кроме того, @carecki, наблюдаемые объекты без подписки вызывают утечку памяти в Angular. Внутри код отписки фактически просто устанавливает переменную в null, чтобы избежать утечки памяти (поскольку объекты GC освобождаются из памяти, когда они равны нулю).

luiscla27 28.04.2022 02:37

@ luiscla27 забавно, что сама корпорация Angular предлагает некоторые «плохие практики», не так ли? angular.io/ошибки/NG0100

markzzz 02.05.2022 10:55

@ luiscla27: • «внешний» угловой жизненный цикл? Какая? Вот как работает асинхронный код. И никаких циклических вызовов, так как ngAfterViewInit вызывается только один раз для каждого компонента. • вы связали код с классом Subject, так не каждый наблюдаемый обрабатывает отмену подписки. Например, Observables, созданные angular httpModule, отменяют текущие http-вызовы. Обычно наблюдаемые объекты удаляют ссылки на своих подписчиков, что позволяет избежать утечек памяти.

carecki 02.05.2022 13:01

Согласитесь с тем, как работает асинхронный код, я сказал «очень похоже на циклические вызовы», а не на циклические вызовы, жизненный цикл идеально проходит одну фазу за другой, сценарий, который я помню, - это когда вы снова вызываете detectChanges, вызывая setTimeout, вы получаете " своего рода циклический вызов», поскольку первый вызов запускает второй жизненный цикл, и снова и снова.. это случилось со мной только один раз и было воспринято как проблема с производительностью (не бесконечный цикл), это не бесконечно, потому что setTimeout не "синхронизируется" с циклом, в моем случае "цикл" срабатывал случайным образом от 9 до 21 раза

luiscla27 02.05.2022 16:41

Сценарий, который я описал в предыдущем комментарии, был головной болью @markzzz, так что мне не смешно :/

luiscla27 02.05.2022 16:43

@markzzz, видео показывает, как избежать ошибки ExpressionChangedAfterItHasBeenCheckedError и объясняет ее причины, но также, основываясь на описанном мной сценарии, плохой практикой было бы специально использовать setTimeout, когда вы потенциально можете инициировать вложенные вызовы detectChanges (как один может спровоцировать другой). поскольку «потенциально» может быть равно «всегда», и поскольку избежать этого не так уж сложно, просто сделайте это. Есть и другие сценарии, с которыми я сталкивался, чтобы возражать против использования setTimeout в качестве «хорошей практики» в Angular, в этом разделе комментариев недостаточно места, чтобы опубликовать их.

luiscla27 02.05.2022 18:59

О, кстати... Я только что понял, что неправильно понял ngAfterViewInit, вы правы насчет циклических вызовов @carecki, однако я все же надеюсь, что вы поняли мою точку зрения, поскольку проблема возникнет, когда разработчик по ошибке активирует setTimeout любым ngDoCheck хуком вызов.

luiscla27 02.05.2022 23:37

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