«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)); } }Блоги
Рекомендаций по книгам
Читайте также