Що таке React
Що таке React
🎯 Мета лекції
- Зрозуміти філософію React: декларативний підхід проти імперативного.
- Усвідомити, що таке Virtual DOM та як він працює.
- Опанувати процес reconciliation — як React визначає, що змінилося у DOM.
- Навчитися розрізняти React Elements та DOM-елементи.
- Зрозуміти однонаправлений потік даних (unidirectional data flow).
🔑 Ключові терміни
- Декларативне програмування (declarative programming) — опис що повинно бути, а не як це зробити.
- Імперативне програмування (imperative programming) — покрокові інструкції як змінити стан системи.
- Virtual DOM — легковажне JavaScript-представлення реального DOM-дерева.
- Reconciliation — процес порівняння попереднього та нового Virtual DOM для визначення мінімальних змін.
- React Element — незмінний JavaScript-об'єкт, що описує вузол UI (результат JSX або
React.createElement()).
🔗 Офіційний референс React:
Швидкий старт · Мислення в стилі React
React — це бібліотека, а не фреймворк
React — це JavaScript-бібліотека для побудови користувацьких інтерфейсів, створена командою Meta (Facebook) у 2013 році. Ключове слово тут — саме бібліотека, а не фреймворк.
Різниця між бібліотекою та фреймворком:
- Бібліотека — це набір інструментів, які ви викликаєте у своєму коді, коли вам це потрібно. Ви контролюєте архітектуру застосунку.
- Фреймворк — це цілісна система, яка викликає ваш код у певних точках. Фреймворк диктує структуру проєкту, систему маршрутизації, управління станом.
React зосереджений на одній задачі — ефективному рендерингу UI. Він не нав'язує:
- Систему маршрутизації (можна використати React Router, TanStack Router або інше).
- Підхід до управління станом (Redux, Zustand, MobX, Context API).
- Спосіб взаємодії з сервером (Axios, Fetch API, RTK Query, TanStack Query).
- Інструменти збірки (Vite, Webpack, Parcel, Rollup).
Це дає гнучкість, але вимагає прийняття рішень. Саме тому існують full-stack React-фреймворки (Next.js, Remix), які додають опінійовану структуру поверх React.
Імперативний підхід: як ми маніпулювали DOM раніше
Перш ніж зрозуміти, що пропонує React, потрібно побачити проблему, яку він вирішує.
Приклад: лічильник на чистому JavaScript
Уявімо простий лічильник із кнопкою "+1". У імперативному підході ми вручну знаходимо DOM-елементи, оновлюємо їхній вміст та додаємо обробники подій.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Імперативний лічильник</title>
<style>
body {
font-family: system-ui, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: #f8fafc;
}
.counter {
background: white;
padding: 2rem;
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
text-align: center;
}
.value {
font-size: 3rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 1rem;
}
button {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #2563eb;
}
</style>
</head>
<body>
<div class="counter">
<div class="value" id="count">0</div>
<button id="increment">Збільшити +1</button>
</div>
<script>
// Імперативний підхід: покрокові інструкції
let count = 0;
// Крок 1: Знайти DOM-елементи
const countElement = document.getElementById('count');
const button = document.getElementById('increment');
// Крок 2: Додати обробник події
button.addEventListener('click', () => {
// Крок 3: Оновити стан
count = count + 1;
// Крок 4: ВРУЧНУ оновити DOM
countElement.textContent = count;
});
</script>
</body>
</html>
Проблеми цього підходу:
- Ручна синхронізація стану та UI:
Кожного разу, коли змінюєтьсяcount, ми вручну оновлюємоcountElement.textContent. У великому застосунку це призводить до помилок — легко забути оновити якийсь елемент. - Розпорошена логіка:
Структура HTML знаходиться в одному місці, а логіка оновлення — в іншому. Важко відстежити, де саме змінюється DOM. - Неефективність при масових оновленнях:
Якщо потрібно оновити 100 елементів, ми викликаємо 100 операцій DOM, кожна з яких тригерить перерахунок layout (reflow) та перемальовування (repaint). - Відсутність композиції:
Важко повторно використовувати логіку. Якщо потрібні два лічильники, доведеться дублювати код або писати складні функції з параметрами.
element.textContent = ... або element.classList.add(...) може викликати:- Reflow (перерахунок layout) — браузер визначає розміри та позиції всіх елементів.
- Repaint (перемальовування) — браузер малює пікселі на екрані.
Декларативний підхід React: опис результату, а не процесу
React пропонує радикально інший підхід: замість того, щоб інструктувати браузер, як змінити DOM, ми описуємо, як повинен виглядати UI для заданого стану.
Той самий лічильник у React
Збирання React застосунку…
Що тут відбувається:
useState(0)— створює змінну стануcountіз початковим значенням0. React запам'ятовує це значення між рендерами.<div className="value">{count}</div>— декларативний опис: "відобрази поточне значенняcount". Ми не пишемоelement.textContent = countвручну.onClick={() => setCount(count + 1)}— коли користувач натискає кнопку, викликаєтьсяsetCountіз новим значенням. React автоматично перерендерить UI.- React бере на себе синхронізацію:
Ми описали, що повинно бути на екрані для заданогоcount. React сам визначає, як оновити DOM.
- Імперативно: "Знайди елемент із
id="count", встанови йогоtextContentу5." - Декларативно: "Коли
countдорівнює5, відобрази<div>5</div>."
Що таке React Element?
Коли ви пишете JSX, React перетворює його на React Element — простий JavaScript-об'єкт, що описує вузол інтерфейсу.
const element = <h1 className="title">Привіт, React!</h1>;
Після компіляції це стає:
const element = {
type: "h1",
props: {
className: "title",
children: "Привіт, React!"
},
key: null,
ref: null
};
React Element — це НЕ DOM-елемент. Це лише опис того, що повинно бути у DOM. React Element:
- Незмінний (immutable) — після створення його не можна змінити.
- Легковажний — це звичайний JavaScript-об'єкт у пам'яті.
- Дешевий у створенні — створення тисяч React Elements швидше, ніж одна DOM-операція.
React читає дерево React Elements та створює або оновлює реальний DOM відповідно до них.
Virtual DOM: легковажна копія реального DOM
Virtual DOM — це JavaScript-представлення реального DOM-дерева. Це дерево React Elements, яке React тримає у пам'яті.
Як працює Virtual DOM
- Початковий рендер:
React створює дерево React Elements (Virtual DOM) на основі вашого JSX. - React перетворює Virtual DOM на реальний DOM:
React викликає DOM API (document.createElement,element.appendChildтощо) та створює реальні вузли. - Користувач взаємодіє з UI:
Натискає кнопку, вводить текст — викликаєтьсяsetStateабоsetCount. - React створює нове дерево Virtual DOM:
React повторно виконує вашу функцію (наприклад,App()) та отримує нове дерево React Elements. - Reconciliation — порівняння старого та нового дерева:
React порівнює попередній Virtual DOM із новим та визначає мінімальний набір змін. - React застосовує зміни до реального DOM:
Замість того, щоб перестворювати весь DOM, React змінює лише те, що дійсно змінилося.
Візуалізація процесу
Чому це ефективно?
- Батчінг оновлень: React групує кілька викликів
setStateв один перерендер. - Мінімізація DOM-операцій: Змінюється лише те, що дійсно змінилося.
- Розумне порівняння: React використовує евристики (наприклад,
keyу списках), щоб уникнути непотрібних оновлень.
<div>, пряме element.textContent = value буде швидшим, ніж Virtual DOM.Але у реальних застосунках UI складається з десятків або сотень компонентів, які залежать один від одного. Вручну відстежувати, які саме DOM-вузли потрібно оновити, стає нереально складно.Virtual DOM дає передбачуваність і підтримуваність коду, жертвуючи мікросекундами продуктивності на простих операціях.Reconciliation: алгоритм порівняння дерев
Reconciliation (узгодження) — це процес, у якому React визначає, що змінилося між попереднім та новим Virtual DOM.
Наївний підхід: повне перерендерювання
Найпростіше рішення — завжди перестворювати весь DOM:
// Наївний підхід
function update() {
const root = document.getElementById('root');
root.innerHTML = ''; // Видалити все
root.appendChild(createNewDOM()); // Створити з нуля
}
Проблеми:
- Повільно: Видалення та створення DOM-вузлів — дорога операція.
- Втрата стану: Фокус у
<input>, позиція скролу, анімації — все скидається. - Тригери reflow/repaint: Браузер перераховує layout усієї сторінки.
Підхід React: порівняння дерев (diffing)
React порівнює старе та нове дерево вузол за вузлом та застосовує мінімальні зміни.
Основні правила reconciliation:
- Різні типи елементів → повна заміна:
// Старе дерево <div>Контент</div> // Нове дерево <span>Контент</span>
React видалить<div>та створить<span>з нуля, включно з усіма дочірніми вузлами. - Однакові типи → оновлення атрибутів:
// Старе дерево <div className="box" style={{ color: 'red' }}>Текст</div> // Нове дерево <div className="card" style={{ color: 'blue' }}>Текст</div>
React залишає той самий DOM-вузол, але оновлюєclassNameтаstyle. - Списки елементів — важливість
key:
Коли React бачить масив елементів, він використовує атрибутkey, щоб зрозуміти, які елементи додані, видалені або переміщені.// Без key — React не знає, що це ті самі елементи {items.map(item => <div>{item.name}</div>)} // З key — React розуміє ідентичність елементів {items.map(item => <div key={item.id}>{item.name}</div>)}
key критично важливий у списках?Без key React припускає, що порядок елементів у масиві відповідає їхній ідентичності. Якщо ви видалите перший елемент, React подумає, що всі елементи змінилися, і оновить увесь список.З key React розуміє: "елемент із key="user-5" перемістився з позиції 0 на позицію 2" — і просто переміщує DOM-вузол, замість того щоб перестворювати його.Детально про key ми поговоримо у лекції про списки.Приклад reconciliation
Збирання React застосунку…
У цьому прикладі React не перестворює <div>, <p> або навіть не змінює їхні класи. Він лише оновлює textContent усередині <p>. Відкрийте DevTools → Elements та подивіться, як мигає лише текстовий вузол, а не весь елемент.
Однонаправлений потік даних (Unidirectional Data Flow)
React дотримується принципу однонаправленого потоку даних: дані рухаються зверху вниз по дереву компонентів.
Схема потоку даних
┌─────────────────────────────────────────────────┐
│ Батьківський компонент (App) │
│ const [user, setUser] = useState({...}) │
│ │
│ Стан (state) зберігається тут ↑ │
└────────────────┬────────────────────────────────┘
│
│ Передає дані через props ↓
↓
┌─────────────────────────────────────────────────┐
│ Дочірній компонент (UserProfile) │
│ function UserProfile({ user }) { ... } │
│ │
│ Отримує дані, але НЕ може змінити їх напряму │
└─────────────────────────────────────────────────┘
Правила:
- Дані передаються вниз через props:
Батьківський компонент передає дані дочірньому через атрибути (пропси). - Дочірній компонент не може змінити props напряму:
Props — це read-only. Спроба змінити їх призведе до помилки у StrictMode. - Зміни стану ініціюються через callback-функції:
Якщо дочірній компонент хоче змінити дані батьківського, батьківський передає йому функцію (setUser), яку дочірній викликає.
Приклад: передача стану вниз
Збирання React застосунку…
Що відбувається:
- Стан (
name,age) зберігається у батьківському компонентіApp. Appпередає ці значення уUserCardчерез props:<UserCard name={name} age={age} />.UserCardне може змінитиnameабоageнапряму — вони read-only.- Коли користувач вводить дані у
<input>, викликаєтьсяsetNameабоsetAgeу батьківському компоненті. - React перерендерює
App, який передає нові значення уUserCard.
Від одного HTML-файлу до повноцінного проєкту
Еволюція від CDN та Babel у браузері до сучасного Vite + React + TypeScript. Розуміння того, як JSX потрапляє до браузера та чому інструменти збірки критично важливі для продакшн-розробки.
JSX та TSX — синтаксичний цукор, а не магія
Глибоке розуміння JSX та TSX — як саме JSX перетворюється на виклики функцій, що таке React.createElement та сучасний jsx-runtime, різниця між HTML та JSX, особливості типізації TSX у TypeScript та найпоширеніші помилки початківців.