for на користь .map().key: ідентифікація, стабільність та перевпорядкування.&&), тернарні оператори та early returns.Методи ітерації по масивах даних для генерації списків компонентів. Важливість атрибута key для алгоритму дифінгу React та наслідки використання індексів масиву як ключів. Розглядаються патерни умовного рендерингу за допомогою логічних операторів (&&) та тернарних операторів для створення гнучких інтерфейсів.
Генерація UI на основі масивів даних (векторів/колекцій) — це найпоширеніша операція у будь-якому веб-додатку (від стрічки новин до таблиць аналітики). Якби для кожного нового елемента в масиві (наприклад, при отриманні нового повідомлення в чаті) React перестворював увесь список у DOM з нуля, продуктивність впала б до неприйнятних значень. У цій лекції ми розберемо комп’ютерну інженерію (Computer Science) за лаштунками роботи алгоритму Heuristic Diffing, який дозволяє React вибірково мутувати вузли списків.
.map() як стандарт декларативностіВ імперативному програмуванні звичною практикою є створення циклу for, всередині якого розробник створює DOM-вузли і додає їх до контейнера:
// Імперативний підхід Vanilla JS
const container = document.getElementById("list");
for (let i = 0; i < data.length; i++) {
const li = document.createElement("li");
li.textContent = data[i].name;
container.appendChild(li); // Пряма мутація DOM під час ітерації!
}
Декларативний підхід React:
Оскільки JSX — це вирази (Expressions), а не інструкції (Statements), ми не можемо писати цикли for прямо всередині JSX. Натомість, ми застосовуємо метод масиву .map(), який трансформує вхідний масив даних у вихідний масив React-компонентів в процесі обчислення функції рендеру, НЕ зачіпаючи реальний DOM.
const UserList = ({ users }) => {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Сам масив [<li>Аня</li>, <li>Богдан</li>] розгортається рендерером автоматично.
У класичних алгоритмах на графах (Tree Edit Distance), порівняння двох дерев (нового Virtual DOM і старого Virtual DOM) займає час O(n³), де n — кількість елементів. Для 1000 елементів це 1 мільярд операцій!
React робить алгоритмічне припущення (Евристику) і зменшує складність до O(n) (1000 операцій). У списках він працює так:
[A, B] -> [A, B, C], React бачить зміни лише в кінці і просто створює DOM-вузол C.[A, B] -> [C, A, B], або відсортувати його, “наївне” порівняння по порядку зазнає краху. React побачить: першим був A, став C (треба переробити), другим був B, став A (треба переробити)… Він перестворить ВЕСЬ список у DOM, змарнувавши купу ресурсів.key: ідентифікація та стабільністьЩоб вирішити цю комп’ютерну проблему вставки не в кінець масиву, розробники React впровадили маркер key.
Ключ (key) — це спеціальний рядок-ідентифікатор, який повідомляє алгоритму стану: “Цей конкретний об’єкт є сутністю X як у старому, так і в новому дереві”.
// Нове дерево генерується так:
<ul>
<li key="C">Елемент C (новий)</li>
<li key="A">Елемент A</li>
<li key="B">Елемент B</li>
</ul>
Коли React бачить key, алгоритм змінюється:
Він перевіряє ключі. “Ага, вузли з ключами ‘A’ і ‘B’ вже існують в пам’яті (у старому дереві). Я не буду їх видаляти та створювати наново (дорогі DOM-операції). Я просто переміщу (mutate) їх фізично нижче, і створю лише новий вузол ‘C’.”
Критерії правильного ключа:
key={Math.random()}) під час рендеру.Якщо ви не передасте key, React буде лаятись червоним повідомленням в консоль, але як запасний варіант (Fallback) візьме індекс масиву (0, 1, 2…). Багато недосвідчених розробників роблять це усвідомлено: users.map((user, index) => <li key={index}>...</li>).
Чому це знищує продуктивність та логіку: Уявіть список з локальним станом:
<input value="Apple"/>)]<input value="Banana"/>)]Ми видаляємо “Яблуко”. “Банан” піднімається вгору, і тепер його індекс стає 0! React порівнює ключі:
value="Apple") залишиться незмінним, бо React подумає, що ви просто редагуєте поточний вузол, а не видалили попередній.Правило 4-го курсу: Індекси як ключі дозволені ТІЛЬКИ для статичних списків, які ніколи не сортуються, не фільтруються і не приймають нових елементів на початку/всередині масиву.
На відміну від шаблонізаторів типу Angular/Vue (які використовують зовнішні директиви типу v-if, ng-if), у деклараційній парадигмі React (JSX) ми є інженерами, які пишуть чистий JavaScript-код.
Є три основні патерни переривання(умовностей) генерації UI:
Ідеально підходить, коли сторінка завантажується або є критична помилка. Повністю блокує виконання подальшого (ймовірно “важкого”) рендеру.
const UserProfile = ({ user }) => {
if (!user) {
return <Spinner />; // Функція рендеру зупиняється ТУТ
}
return <ComplexDashboard user={user} />;
};
if-else)Оскільки всередині блока {} в JSX дозволені лише вирази (Expressions), ми використовуємо тернарні оператори.
return (
<div className={isActive ? "tab-active" : "tab-hidden"}>
{isLoggedIn ? <LogoutButton /> : <LoginForm />}
</div>
);
&&)Коли нам не потрібна альтернативна гілка (блок else).
return (
<div>
<h1>Профіль</h1>
{user.isAdmin && <AdminPanel />}
</div>
);
При використанні логічного оператора && React має певне правило щодо Falsy значень (тих, що є “візуальною порожнечею” або хибністю):
true, false, null, undefined — React просто ігнорує їх і виводить “ніщо” (порожній рядок у HTML).Небезпека чисел 0 (Zero Falsy Bug):
// ПОМИЛКА: Якщо кошик порожній (.length = 0), React відрендерить цифру "0" на сторінці!
const Cart = ({ items }) => {
return <div>{items.length && <CheckoutButton />}</div>;
};
Оскільки 0 є числом, логіка зупиняється на першому операнді (0 — це falsy), але React ВМІЄ малювати числа на екран. Тому він мовчки виводить 0!
Правильні рішення в Інженерії:
Boolean (через подвійне заперечення): !!items.length && <Button />items.length > 0 && <Button />items.length ? <Button /> : nullМи знаємо про алгоритм Diffing і key. Але що, якщо нам потрібно відрендерити стрічку активності (Log viewer), де 100 000 текстових записів? Навіть якщо ключі ідеальні, створення 100 000 DOM-вузлів призведе до того, що вкладка браузера споживе до 2 ГБ оперативної пам’яті (RAM Allocation) і напевно “впаде” (Crash).
Для вирішення такої архітектурної катастрофи застосовується патерн Virtualization (Віртуалізація списків/Віконний рендеринг). Замість рендерингу всіх 100 000 елементів, ми рендеримо лише ті 15, які фізично видно на екрані зараз (View Port), і ще кілька “про запас” зверху і знизу. Як тільки користувач скролить, ми підміняємо дані в цих же 15 DOM-вузлах і зсуваємо їх позицію (через CSS Transform) або перезаписуємо контент, не створюючи нові вузли.
Цей патерн зазвичай не пишуть з нуля, а використовують високорівневі бібліотеки, такі як react-virtualized або react-window, інтеграція яких показує високий інженерний рівень (Senior level/Architecture understanding) розробника.
.map().key дає йому критичну інформацію, що дозволяє мутувати (переміщати) вузли замість їх дорогого перестворення.&& та їх особливості роботи із цілими числами (0).react-window).react-window library repository. Github. URL: https://github.com/bvaughn/react-windowkey додавання елемента на початок масиву перетворює операцію маніпуляції DOM на катастрофічно неефективну дію?.map((item, id) => ...) як ключа допустиме і не спричинить багів відображення/стану інтерфейсу.<div> {items.length && <List/>} </div> із порожнім масивом [] залишить на екрані цифру 0? Як інженерно грамотно нейтралізувати цю проблему?display: none) та приховуванням його через механізм умовного рендерингу React (!isHidden && <Element/>)? Який з підходів є більш жорстким до життєвого циклу (Lifecycle)?