Angular IDPStrong Junior
Модулі / Точний layout і Change Detection / Чому мутація ламає OnPush
🔥 0

Модуль 3 · урок 3 з 11

Чому мутація ламає OnPush

Це найкоротший урок модуля й найважливіший на практиці. Дев'ять із десяти проблем з OnPush — це мутація там, де мала бути нова копія. Розберемось один раз, і далі цей клас помилок зникне.

Чому Angular не помічає зміну

Перевірка input — це буквально одне порівняння: prev === next. Для обʼєктів і масивів воно порівнює адресу в памʼяті, а не вміст. Тому:

МУТАЦІЯ tasks.push(newTask) масив змінився, адреса — ні prev === next → true компонент пропущено, екран старий НОВА КОПІЯ tasks = [...tasks, newTask] новий масив — нова адреса prev === next → false компонент перевірено й перемальовано Те саме правило діє й для сигналів: signal порівнює значення так само, за посиланням.
Angular не обходить обʼєкт у пошуках змін — він звіряє одне посилання. Дешево, передбачувано, але вимагає працювати з незмінними даними.

Словник незмінних операцій

// ДОДАТИ
list = [...list, item];                      // у кінець
list = [item, ...list];                      // на початок
list = [...list.slice(0, i), item, ...list.slice(i)];  // у середину

// ВИДАЛИТИ
list = list.filter(t => t.id !== id);

// ЗМІНИТИ ОДИН ЕЛЕМЕНТ
list = list.map(t => t.id === id ? { ...t, status: 'done' } : t);

// ВІДСОРТУВАТИ (sort мутує!)
list = [...list].sort((a, b) => a.order - b.order);

// ОБʼЄКТ
task = { ...task, title: 'Нова назва' };
task = { ...task, meta: { ...task.meta, tag: 'x' } };   // вкладений рівень теж копіюємо

// ВИДАЛИТИ ПОЛЕ
const { removed, ...rest } = task;
task = rest;

Головна деталь: не копіюй зайвого

// ❌ так усі 1000 карток перемалюються, хоч змінилась одна
list = list.map(t => ({ ...t, status: t.id === id ? 'done' : t.status }));

// ✅ так перемалюється рівно одна
list = list.map(t => t.id === id ? { ...t, status: 'done' } : t);

Різниця тонка, але принципова. У першому варіанті кожен елемент — новий обʼєкт, тож кожна дитяча картка бачить нове посилання й перевіряється. У другому незмінені елементи зберігають свої посилання, і Angular пропускає їх. Разом із track із модуля 2 це і дає таблицю, яка не лагає.

Мутація сама по собі не зло

Усередині функції, поки обʼєкт нікуди не потрапив, мутувати можна скільки завгодно — зібрав масив у циклі й повернув. Правило стосується лише спільного стану: того, що вже передане в компоненти або лежить у сигналі. Там мутація — це мовчазний баг.

structuredClone і глибина

const copy = { ...task };            // поверхнева: вкладені обʼєкти спільні
const deep = structuredClone(task);  // повна копія всього дерева

Глибока копія потрібна рідко й коштує дорого. У 95% випадків достатньо скопіювати рівно ті рівні, які змінюєш, — саме тому в прикладі вище копіюється task і task.meta, а решта лишається спільною.

Як зловити мутацію ще до того, як вона зʼїсть вечір

// у моделі: заборонити зміну на рівні типів
readonly tasks = signal<readonly Task[]>([]);

function render(rows: readonly Task[]) {
  rows.push(x);        // ❌ помилка компіляції
}

// у дев-режимі: заморозити й отримати помилку в рантаймі
const frozen = Object.freeze(structuredClone(data));

Перший спосіб робочий і безкоштовний: readonly Task[] у сигнатурі presentation-компонента — це і документація, і перевірка. Другий — для налагодження, коли мутація вже десь є, а де саме, незрозуміло.

≈ 35 хв · +25 XP за урок