Expression has changed after it was checked: как победить ошибку в Angular

Онлайн-тренажер Python для начинающих

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

Начать курс

«Expression has changed after it was checked» — почему это происходит и как с этим жить?

Если вы когда-либо писали приложение на Angular, то наверняка сталкивались с этим зловещим сообщением в консоли:

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.

Эта ошибка — настоящий «бич» разработчиков. Тысячи вопросов на Stack Overflow, сотни часов отладки, десятки нервных срывов. Но хорошая новость: её можно не только исправить, но и понять, почему она возникает. В этой статье мы разберём топ причин ошибки Expression has changed after it was checked, напишем примеры на TypeScript и дадим конкретные решения. Поехали!

Что вообще означает эта ошибка?

Angular работает в двух режимах: разработка (dev mode) и продакшн. В dev-режиме фреймворк выполняет дополнительную проверку — после каждого цикла обнаружения изменений он запускает второй проход, чтобы убедиться, что данные не изменились «на лету». Если значение изменилось между первым и вторым проходом — Angular падает с ошибкой ExpressionChangedAfterItHasBeenCheckedError.

Простыми словами: вы сказали Angular «вот такие данные», он их проверил, а потом вы их поменяли. Angular обижается и кричит.

Топ 3 причины ошибки

1. Изменение данных в хуках жизненного цикла после ngAfterViewInit

Самая популярная причина — попытка изменить свойство компонента в хуке ngAfterViewInit, ngAfterContentInit или ngAfterViewChecked. В этот момент Angular уже завершил проверку представления, и любое изменение вызывает ошибку.

Пример на TypeScript:

import { Component, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-example',
  template: '<p>{{ message }}</p>'
})
export class ExampleComponent implements AfterViewInit {
  message = 'Начальное значение';

  ngAfterViewInit(): void {
    // Ошибка! Изменяем свойство после проверки представления
    this.message = 'Изменённое значение';
  }
}

Здесь Angular сначала проверяет message (значение «Начальное значение»), затем вызывает ngAfterViewInit, где вы меняете его на «Изменённое значение». При повторной проверке Angular видит расхождение и выбрасывает ошибку.

2. Асинхронные операции внутри жизненного цикла

Если вы запускаете асинхронный код (setTimeout, Promise, Observable) внутри хука, то обновление данных происходит вне зоны контроля Angular.

Пример:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-async',
  template: '<p>{{ data }}</p>'
})
export class AsyncComponent implements OnInit {
  data = 'Старт';

  ngOnInit(): void {
    setTimeout(() => {
      this.data = 'Новое значение';
    }, 0);
  }
}

Хотя setTimeout с задержкой 0 мс выполняется почти сразу, Angular уже завершил проверку. В dev-режиме это приведёт к ошибке.

3. Вложенные компоненты и передача данных через Input

Когда родительский компонент передаёт данные дочернему через @Input, а дочерний компонент пытается изменить эти данные в своём жизненном цикле, возникает конфликт.

Пример:

// Родительский компонент
@Component({
  selector: 'app-parent',
  template: '<app-child [value]="parentValue"></app-child>'
})
export class ParentComponent {
  parentValue = 'Исходное';
}

// Дочерний компонент
@Component({
  selector: 'app-child',
  template: '<p>{{ value }}</p>'
})
export class ChildComponent implements AfterViewInit {
  @Input() value: string = '';

  ngAfterViewInit(): void {
    this.value = 'Изменённое в дочернем';
  }
}

Здесь дочерний компонент изменяет входное свойство после того, как Angular проверил родительское свойство. Ошибка гарантирована.

Как исправить ошибку Expression has changed after it was checked?

У нас есть несколько проверенных способов. Выбирайте по ситуации.

Способ 1: Использовать ChangeDetectorRef.detectChanges()

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

import { Component, AfterViewInit, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-example',
  template: '<p>{{ message }}</p>'
})
export class ExampleComponent implements AfterViewInit {
  message = 'Начальное значение';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    this.message = 'Изменённое значение';
    this.cdr.detectChanges(); // Принудительная проверка
  }
}

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

Способ 2: Использовать setTimeout()

Классический «костыль», который работает. Вы просто выносите изменение данных в следующий макротаск.

import { Component, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-async',
  template: '<p>{{ data }}</p>'
})
export class AsyncComponent implements AfterViewInit {
  data = 'Старт';

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.data = 'Новое значение';
    });
  }
}

Но будьте осторожны: это может вызвать мерцание UI, так как Angular сначала отобразит старое значение, а потом — новое.

Способ 3: Использовать NgZone.runOutsideAngular()

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

import { Component, NgZone } from '@angular/core';

@Component({
  selector: 'app-zone',
  template: '<p>{{ count }}</p>'
})
export class ZoneComponent {
  count = 0;

  constructor(private ngZone: NgZone) {}

  increment(): void {
    this.ngZone.runOutsideAngular(() => {
      // Этот код не влияет на Angular
      setTimeout(() => {
        this.ngZone.run(() => {
          this.count++;
        });
      }, 1000);
    });
  }
}

Этот способ полезен для оптимизации — например, когда вы обрабатываете много событий мыши или скролла и не хотите вызывать лишние проверки.

Способ 4: Использовать async pipe и Observable

Лучший способ — не бороться с ошибкой, а избегать её архитектурно. Используйте async пайп и Observable для асинхронных данных.

import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Component({
  selector: 'app-async-pipe',
  template: '<p>{{ data$ | async }}</p>'
})
export class AsyncPipeComponent implements OnInit {
  data$: Observable<string>;

  ngOnInit(): void {
    this.data$ = of('Новое значение').pipe(delay(0));
  }
}

                
                

Блоги

Рекомендаций по книгам