Avatar ·

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

Всем привет! Я новичок в Angular и TypeScript, и столкнулся с очень странной ошибкой, которая вылетает в консоли браузера: "Expression has changed after it was checked". У меня есть простой компонент, который отображает список задач. Вот упрощённый код: ```typescript import { Component, OnInit } from '@angular/core'; interface Task { id: number; title: string; completed: boolean; } @Component({ selector: 'app-task-list', template: `
{{ task.title }} {{ task.completed ? '✅' : '❌' }}

Всего задач: {{ tasks.length }}

` }) export class TaskListComponent implements OnInit { tasks: Task[] = [ { id: 1, title: 'Купить продукты', completed: false }, { id: 2, title: 'Сделать зарядку', completed: true } ]; constructor() {} ngOnInit() { // Проблема здесь: я пытаюсь изменить tasks после инициализации setTimeout(() => { this.tasks.push({ id: 3, title: 'Почитать книгу', completed: false }); }, 0); } toggleTask(id: number) { const task = this.tasks.find(t => t.id === id); if (task) { task.completed = !task.completed; } } } ``` Когда я нажимаю на кнопку "Toggle" или когда срабатывает setTimeout, в консоли появляется ошибка "Expression has changed after it was checked". Я читал, что это связано с режимом разработки, но как это исправить? Почему изменение массива или свойства объекта вызывает такую ошибку? И как правильно обновлять данные в Angular? Буду благодарен за подробное объяснение! <<>>

Что означает ошибка "Expression has changed after it was checked"?

Эта ошибка возникает в Angular, когда значение, используемое в шаблоне, изменяется после того, как Angular завершил проверку изменений (change detection) для текущего цикла. В режиме разработки Angular выполняет дополнительную проверку (change detection) после первого прохода, чтобы убедиться, что данные стабильны. Если за время между двумя проверками значение изменилось — выбрасывается исключение.

В вашем коде проблема в ngOnInit: вы используете setTimeout(() => { ... }, 0). Хотя задержка равна 0, это всё равно асинхронная операция, которая выполняется после того, как Angular завершил первую проверку изменений. Когда setTimeout срабатывает, он добавляет элемент в массив tasks, и Angular обнаруживает, что значение изменилось между проверками — отсюда ошибка.

Почему это происходит?

Angular работает в двух режимах:

  • Режим разработки (dev mode) — выполняется двойная проверка изменений. Если данные меняются между первой и второй проверкой — вылетает ошибка.
  • Production режим — двойной проверки нет, поэтому ошибка не возникает, но логика всё равно остаётся проблемной (данные могут не обновиться в шаблоне вовремя).

Как исправить?

Есть несколько правильных способов обновлять данные в Angular без этой ошибки:

Способ 1: Не изменять данные асинхронно внутри хуков жизненного цикла

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

export class TaskListComponent implements OnInit {
  tasks: Task[] = []; // пустой массив

  ngOnInit() {
    // Синхронное присваивание — безопасно
    this.tasks = [
      { id: 1, title: 'Купить продукты', completed: false },
      { id: 2, title: 'Сделать зарядку', completed: true }
    ];
  }
}

Способ 2: Использовать ChangeDetectorRef для принудительного обновления

Если асинхронная операция неизбежна (например, получение данных с сервера), используйте ChangeDetectorRef.detectChanges() после изменения данных:

import { ChangeDetectorRef } from '@angular/core';

export class TaskListComponent implements OnInit {
  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit() {
    setTimeout(() => {
      this.tasks.push({ id: 3, title: 'Почитать книгу', completed: false });
      this.cdr.detectChanges(); // принудительно запускаем проверку изменений
    }, 1000);
  }
}

Способ 3: Использовать NgZone для выполнения кода внутри зоны Angular

Если вы используете сторонние библиотеки или события вне зоны Angular, оберните код в NgZone.run():

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

export class TaskListComponent implements OnInit {
  constructor(private zone: NgZone) {}

  ngOnInit() {
    this.zone.run(() => {
      setTimeout(() => {
        this.tasks.push({ id: 3, title: 'Почитать книгу', completed: false });
      }, 1000);
    });
  }
}

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

Самый современный и правильный способ — работать с потоками данных через async pipe. Это позволяет Angular автоматически управлять проверкой изменений:

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

@Component({
  selector: 'app-task-list',
  template: `
    
...

Всего задач: {{ (tasks$ | async)?.length }}

` }) export class TaskListComponent implements OnInit { private tasksSubject = new BehaviorSubject<Task[]>([ { id: 1, title: 'Купить продукты', completed: false }, { id: 2, title: 'Сделать зарядку', completed: true } ]); tasks$ = this.tasksSubject.asObservable(); ngOnInit() { setTimeout(() => { const currentTasks = this.tasksSubject.value; this.tasksSubject.next([...currentTasks, { id: 3, title: 'Почитать книгу', completed: false }]); }, 1000); } toggleTask(id: number) { const updatedTasks = this.tasksSubject.value.map(task => task.id === id ? { ...task, completed: !task.completed } : task ); this.tasksSubject.next(updatedTasks); } }

Важные правила для избежания ошибки:

  • Не изменяйте данные, отображаемые в шаблоне, внутри ngAfterViewInit и ngAfterViewChecked
  • Асинхронные операции (setTimeout, Promise, HTTP-запросы) всегда выполняются после текущего цикла проверки
  • Используйте иммутабельные обновления (создавайте новые объекты/массивы вместо мутации)
  • Для сложных сценариев используйте ChangeDetectionStrategy.OnPush в комбинации с Observable

В вашем случае с кнопкой "Toggle" ошибка возникает, потому что вы мутируете объект внутри массива (task.completed = !task.completed). Создайте новый объект для каждого изменения, чтобы Angular мог отследить изменения через ссылки.

<<>>

Альтернативный подход: использовать ChangeDetectionStrategy.OnPush

Если вы хотите более производительное приложение и избежать подобных ошибок в будущем, рекомендую перейти на стратегию OnPush. Она заставляет Angular проверять компонент только при изменении входных данных (@Input) или при явном вызове markForCheck().

Вот как адаптировать ваш компонент:

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

@Component({
  selector: 'app-task-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class TaskListComponent {
  tasks: Task[] = [
    { id: 1, title: 'Купить продукты', completed: false },
    { id: 2, title: 'Сделать зарядку', completed: true }
  ];

  constructor(private cdr: ChangeDetectorRef) {}

  addTask() {
    // Всегда создаём новый массив
    this.tasks = [...this.tasks, { id: 3, title: 'Новая задача', completed: false }];
    this.cdr.markForCheck(); // сообщаем Angular о необходимости проверить компонент
  }

  toggleTask(id: number) {
    this.tasks = this.tasks.map(task =>
      task.id === id ? { ...task, completed: !task.completed } : task
    );
    this.cdr.markForCheck();
  }
}

Преимущества:

  • Меньше проверок — выше производительность
  • Ошибка "Expression has changed" почти не возникает, так как вы сами управляете проверкой
  • Код становится более предсказуемым

Недостаток: нужно не забывать вызывать markForCheck() или detectChanges() после каждого изменения данных. Но это быстро входит в привычку.

Советую начать с этого подхода, если ваш проект новый или вы готовы