Список с ChangeDetectionStrategy.OnPush не обновляется после добавления элемента. Объяснить причину и исправить.
@Component({ selector: 'todo-list', template: ` <div *ngFor="let t of todos">{{ t.title }}</div> <button (click)="add()">Add</button> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class TodoListComponent { @Input() todos: Todo[] = []; add(): void { this.todos.push({ title: 'New' }); // мутация — вид не обновится }}
Проблемы
Что не так
При OnPush Angular запускает проверку компонента только когда:
изменилась ссылка на @Input (сравнение через ===);
сработал DOM-эвент внутри компонента;
эмитнул подписанный через async pipe Observable;
вручную вызван markForCheck().
todos.push(...) мутирует тот же массив — ссылка не меняется. Angular сравнивает старую и новую ссылку, видит равенство и пропускает проверку. Новый элемент в DOM не попадает.
Решение
Решение — иммутабельное обновление
@Component({ selector: 'todo-list', template: ` <div *ngFor="let t of todos">{{ t.title }}</div> <button (click)="add()">Add</button> `, changeDetection: ChangeDetectionStrategy.OnPush,})export class TodoListComponent { @Input() todos: Todo[] = []; @Output() todosChange = new EventEmitter<Todo[]>(); add(): void { // новая ссылка → OnPush увидит изменение this.todos = [...this.todos, { title: 'New' }]; this.todosChange.emit(this.todos); }}
Ключевая идея
OnPush работает на сравнении ссылок. Всегда создавайте новый массив/объект ([...arr], {...obj}) вместо мутации. Это же требование лежит в основе иммутабельного стейта (NgRx, signals) и делает change detection дешёвым и предсказуемым.
Альтернатива — ручной markForCheck
constructor(private cdr: ChangeDetectorRef) {}add(): void { this.todos.push({ title: 'New' }); this.cdr.markForCheck(); // явно помечаем на проверку}
Работает, но императивно и легко забыть. Иммутабельность предпочтительнее.