nmk

Лекція №9 (2 години). Списки, ключі та умовний рендеринг: Динамічна генерація UI та оптимізація дифінгу

План лекції

  1. Патерни ітерації масивів: відмова від циклів for на користь .map().
  2. Алгоритм Reconciliation у списках: як React обчислює різницю (Diffing) між деревами.
  3. Фундаментальна роль атрибута key: ідентифікація, стабільність та перевпорядкування.
  4. Антипатерн “Індекси масиву як Ключі”: втрата стану та деградація продуктивності.
  5. Умовний рендеринг (Conditional Rendering): Логічне І (&&), тернарні оператори та early returns.
  6. Нюанси умовного рендерингу з числами (Falsy values) та масивами.
  7. Віртуалізація списків (Windowing / Virtualization) як архітектурний паттерн для великих даних.

Перелік умовних скорочень

Вступ

Методи ітерації по масивах даних для генерації списків компонентів. Важливість атрибута key для алгоритму дифінгу React та наслідки використання індексів масиву як ключів. Розглядаються патерни умовного рендерингу за допомогою логічних операторів (&&) та тернарних операторів для створення гнучких інтерфейсів.

Генерація UI на основі масивів даних (векторів/колекцій) — це найпоширеніша операція у будь-якому веб-додатку (від стрічки новин до таблиць аналітики). Якби для кожного нового елемента в масиві (наприклад, при отриманні нового повідомлення в чаті) React перестворював увесь список у DOM з нуля, продуктивність впала б до неприйнятних значень. У цій лекції ми розберемо комп’ютерну інженерію (Computer Science) за лаштунками роботи алгоритму Heuristic Diffing, який дозволяє React вибірково мутувати вузли списків.


1. Патерни ітерації масивів: .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>] розгортається рендерером автоматично.


2. Алгоритм Reconciliation у списках (Heuristic Diffing O(N))

У класичних алгоритмах на графах (Tree Edit Distance), порівняння двох дерев (нового Virtual DOM і старого Virtual DOM) займає час O(n³), де n — кількість елементів. Для 1000 елементів це 1 мільярд операцій!

React робить алгоритмічне припущення (Евристику) і зменшує складність до O(n) (1000 операцій). У списках він працює так:

  1. React порівнює дітей (список) спираючись на їх порядок.
  2. Якщо у кінець старого списку додали новий елемент: [A, B] -> [A, B, C], React бачить зміни лише в кінці і просто створює DOM-вузол C.
  3. ПРОБЛЕМА: Якщо вставити елемент НА ПОЧАТОК списку: [A, B] -> [C, A, B], або відсортувати його, “наївне” порівняння по порядку зазнає краху. React побачить: першим був A, став C (треба переробити), другим був B, став A (треба переробити)… Він перестворить ВЕСЬ список у DOM, змарнувавши купу ресурсів.

3. Фундаментальна роль атрибута 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’.”

Критерії правильного ключа:

  1. Унікальність: Ключ має бути унікальним ЛИШЕ серед своїх братів (siblings).
  2. Стабільність: Ключ не повинен змінюватись з плином часу. Не можна використовувати випадкові генератори (на кшталт key={Math.random()}) під час рендеру.

4. Антипатерн “Індекси масиву як Ключі”

Якщо ви не передасте key, React буде лаятись червоним повідомленням в консоль, але як запасний варіант (Fallback) візьме індекс масиву (0, 1, 2…). Багато недосвідчених розробників роблять це усвідомлено: users.map((user, index) => <li key={index}>...</li>).

Чому це знищує продуктивність та логіку: Уявіть список з локальним станом:

Ми видаляємо “Яблуко”. “Банан” піднімається вгору, і тепер його індекс стає 0! React порівнює ключі:

Правило 4-го курсу: Індекси як ключі дозволені ТІЛЬКИ для статичних списків, які ніколи не сортуються, не фільтруються і не приймають нових елементів на початку/всередині масиву.


5. Умовний рендеринг (Conditional Rendering)

На відміну від шаблонізаторів типу Angular/Vue (які використовують зовнішні директиви типу v-if, ng-if), у деклараційній парадигмі React (JSX) ми є інженерами, які пишуть чистий JavaScript-код.

Є три основні патерни переривання(умовностей) генерації UI:

1. Раннє повернення (Early Return)

Ідеально підходить, коли сторінка завантажується або є критична помилка. Повністю блокує виконання подальшого (ймовірно “важкого”) рендеру.

const UserProfile = ({ user }) => {
  if (!user) {
    return <Spinner />; // Функція рендеру зупиняється ТУТ
  }
  return <ComplexDashboard user={user} />;
};

2. Тернарний оператор (Розгалуження if-else)

Оскільки всередині блока {} в JSX дозволені лише вирази (Expressions), ми використовуємо тернарні оператори.

return (
  <div className={isActive ? "tab-active" : "tab-hidden"}>
    {isLoggedIn ? <LogoutButton /> : <LoginForm />}
  </div>
);

3. Логічне І (&&)

Коли нам не потрібна альтернативна гілка (блок else).

return (
  <div>
    <h1>Профіль</h1>
    {user.isAdmin && <AdminPanel />}
  </div>
);

6. Нюанси умовного рендерингу з числами та масивами

При використанні логічного оператора && React має певне правило щодо Falsy значень (тих, що є “візуальною порожнечею” або хибністю):

Небезпека чисел 0 (Zero Falsy Bug):

// ПОМИЛКА: Якщо кошик порожній (.length = 0), React відрендерить цифру "0" на сторінці!
const Cart = ({ items }) => {
  return <div>{items.length && <CheckoutButton />}</div>;
};

Оскільки 0 є числом, логіка зупиняється на першому операнді (0 — це falsy), але React ВМІЄ малювати числа на екран. Тому він мовчки виводить 0!

Правильні рішення в Інженерії:

  1. Явне перетворення на Boolean (через подвійне заперечення): !!items.length && <Button />
  2. Оператор строгого порівняння (Best Practice): items.length > 0 && <Button />
  3. Тернарний оператор (більш читабельно для команд): items.length ? <Button /> : null

7. Віртуалізація списків (Windowing / Virtualization)

Ми знаємо про алгоритм 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) розробника.


Висновки

  1. Ітерація інтрефейсу масивами даних органічно відповідає парадигмі функціонального програмування React, використовуючи математичну прив’язку даних і метод .map().
  2. Алгоритм порівняння дерев (Reconciliation) розрахований на евристичне дослідження O(N). Стабільний і унікальний атрибут key дає йому критичну інформацію, що дозволяє мутувати (переміщати) вузли замість їх дорогого перестворення.
  3. Практика створення ключів на базі індексів масиву у динамічних списках — серйозний архітектурний прорахунок. Він провокує гейзенбаги (heisenbugs) з непередбачуваним змішуванням мутабельного стейту (input values, scroll positions) сусідніх компонентів.
  4. Механізми Conditional Rendering — виразна мова логіки React-компонентів, яка покладається на JavaScript-фундамент: early return, тернарні оператори та логічне порівняння && та їх особливості роботи із цілими числами (0).
  5. Робота з Гіга-масивами даних (Big Data/Logs list rendering) потребує виходу за межі стандартного алгоритму “дифінгу”, вдаючись до складніших рішень з ручним менеджментом інстансів (react-window).

Джерела

  1. Офіційна документація React. “Rendering Lists”. URL: https://react.dev/learn/rendering-lists
  2. React Docs. “Conditional Rendering”. URL: https://react.dev/learn/conditional-rendering
  3. React Docs. “Preserving and Resetting State” (Роль Key у життєвому циклі вузлів). URL: https://react.dev/learn/preserving-and-resetting-state
  4. Dan Abramov. “React as a UI Runtime” (Архітектурний блогпост). URL: https://overreacted.io/react-as-a-ui-runtime/
  5. “Index as a key is an anti-pattern”. Robin Pokorny Blog. URL: https://robinpokorny.com/blog/index-as-a-key-is-an-anti-pattern/
  6. “How React Reconciliation Works”. CSS-Tricks. URL: https://css-tricks.com/how-react-reconciliation-works/
  7. react-window library repository. Github. URL: https://github.com/bvaughn/react-window
  8. Kyle Simpson. “You Don’t Know JS: Types & Grammar” (В контексті глибокого розуміння Falsy values).

Запитання для самоперевірки

  1. З позиції оптимізації складності алгоритмів, чому без передачі маркера key додавання елемента на початок масиву перетворює операцію маніпуляції DOM на катастрофічно неефективну дію?
  2. Опишіть конкретний сценарій розробки в інтернет-магазині, коли використання індексу .map((item, id) => ...) як ключа допустиме і не спричинить багів відображення/стану інтерфейсу.
  3. Чому розгортання умови в JSX у вигляді <div> {items.length && <List/>} </div> із порожнім масивом [] залишить на екрані цифру 0? Як інженерно грамотно нейтралізувати цю проблему?
  4. Якщо ви отримуєте завдання вивести на клієнт таблицю з логами сервера (40 000+ текстових рядків в одному компоненті), який архітектурний підхід варто використати, щоб уникнути Memory Leak та тривалого зависання вікна браузера?
  5. В чому принципова архітектурна відмінність між приховуванням елемента через CSS (display: none) та приховуванням його через механізм умовного рендерингу React (!isHidden && <Element/>)? Який з підходів є більш жорстким до життєвого циклу (Lifecycle)?