Модуль 3 · урок 3 з 11
Чому мутація ламає OnPush
Це найкоротший урок модуля й найважливіший на практиці. Дев'ять із десяти проблем
з OnPush — це мутація там, де мала бути нова копія. Розберемось один раз, і далі
цей клас помилок зникне.
Чому Angular не помічає зміну
Перевірка input — це буквально одне порівняння: prev === next. Для обʼєктів і
масивів воно порівнює адресу в памʼяті, а не вміст. Тому:
Словник незмінних операцій
// ДОДАТИ
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-компонента — це і документація, і перевірка. Другий — для налагодження,
коли мутація вже десь є, а де саме, незрозуміло.