Від скриптів DOM до інтерфейсу як функції стану: основна модель React
Чому React замінює ручні оновлення DOM на декларативні компоненти, та як JSX, props, state, перерендеринг та однонапрямковий потік даних вписуються в єдину концепцію.
Кнопку «Лайк», яка оновлює лічильник, змінює іконку та відтворює анімацію, легко створити за допомогою простих викликів DOM. Однак коли їх п’ятдесят у прямій трансляції, кожна з яких також відстежує коментарі, поширення та збережений стан, ручний код DOM починає втрачати ефективність. React існує саме для вирішення таких проблем: ви описуєте, як має виглядати екран для поточних даних, а React визначає, які зміни DOM необхідні.
У цьому посібнику розглядаються ідеї, які це уможливлюють, у порядку їхнього взаємозв’язку: JSX, компоненти, пропси, стан, перерендеринг, декларативний стиль та спосіб передачі даних та подій через дерево компонентів. До кінця ви зможете передбачати, коли компонент буде перерендерований, визначати, де має знаходитися певний елемент стану, та розпізнавати початкові помилки, які заважають оновленням.
Проблема, яку вирішує React
До того, як бібліотеки компонентів стали нормою, інтерактивний інтерфейс означав використання імперативного коду: потрібно було знаходити елементи та самостійно вносити кожну зміну щоразу, коли щось відбувалося. Для однієї кнопки «Лайк» налаштування виглядають так:
// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;
А обробник кліку мусить пам’ятати кожну візуальну деталь обох станів:
button.addEventListener("click", function() {
if (!liked) {
liked = true;
count++;
countEl.textContent = count;
button.classList.add("liked");
button.classList.remove("unliked");
icon.src = "/icons/heart-filled.svg";
button.style.color = "#e0245e";
// trigger animation
button.classList.add("animate-pop");
setTimeout(() => button.classList.remove("animate-pop"), 300);
} else {
liked = false;
count--;
countEl.textContent = count;
button.classList.remove("liked");
button.classList.add("unliked");
icon.src = "/icons/heart-empty.svg";
button.style.color = "#6c757d";
}
});
Це ще під контролем для однієї кнопки. Тепер уявіть ленту з 50 публікаціями, кожна з яких має власні кнопки «Лайк», «Коментар», «Поділитися» та «Зберегти», причому всі вони можуть змінюватися через кліки користувача або через надходження нових даних з сервера. Скрипт перетворюється на мережу пошуків елементів, слухачів та змінних, які потрібно вручну підтримувати у синхронності. З ростом додатку з’являються три можливі сценарії збою:
- Синхронізація. DOM відображає одне, а ваші змінні — інше. Ви оновлюєте елемент лічильника, але забуваєте про змінну, або навпаки, і тепер у вас є два джерела інформації, які потрібно виправляти під час дебагування.
- Многоразове використання. Обробник подій прив’язаний до конкретних ідентифікаторів елементів та певної структури HTML, тому переміщення кнопки на іншу сторінку означає її переписування.
- Складність. Кожна нова функція змушує вас розуміти все інше, що може бути пов’язане з нею. Навіть невеликі зміни можуть пошкодити віддалені частини сторінки.
React, відкритий для користування Facebook у 2013 році, дає відповідь на всі три проблеми за допомогою однієї ідеї: припинити відправляти команди DOM та замість цього описувати інтерфейс, який має існувати у певному стані. React порівнює цей опис із тим, що відображається на екрані, та вносить необхідні зміни. Ви описуєте; React оновлює.
JSX: маркап, який компілюється у JavaScript
Першою незвичною річчю у коді React є маркап, схожий на HTML, всередині функції:
function Greeting() {
return (
<div className="greeting">
<h1>Hello, Priya!</h1>
<p>Welcome back.</p>
</div>
);
}
Цей маркап — це JSX, розширення синтаксису, яке дозволяє писати структури елементів у файлах JavaScript. Це не HTML і не щось, що розуміє браузер. На етапі компіляції (Babel, компілятор TypeScript або інструмент для об’єднання коду, який використовує їх) цей маркап переписується у звичайні виклики функцій ще до того, як код буде виконаний. Ви пишете:
// What you write (JSX):
return (
<h1 className="title">Hello!</h1>
);
а компілятор створює щось еквівалентне:
// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");
React.createElement просто повертає звичайний об’єкт, який описує елемент: його тип, властивості та дочірні елементи. React читає ці об’єкти, щоб вирішити, що має містити справжній DOM. (У новіших версіях JSX використовуються інші допоміжні функції з react/jsx-runtime, але результат — це той самий тип описового об’єкта.) Ніхто не пише ці виклики вручну; JSX існує тому, що вкладені маркапи значно легше читати.
У чому JSX відрізняється від HTML
Синтаксис схожий на HTML, але не ідентичний. Найбільш поширені відмінності, які створюють труднощі:
// HTML JSX
// ───────────────────────────── ──────────────────────────────────
// class="title" className="title" ← JS reserved word
// for="email" htmlFor="email" ← JS reserved word
// <input> (self-closing optional) <input /> ← must self-close
// onclick="handler()" onClick={handler} ← camelCase, no quotes
// style="color: red" style={{ color: "red" }} ← JS object
class та for є зарезервованими словами в JavaScript, тому JSX використовує className та htmlFor. Кожен елемент мусить бути закритий. Обробники подій пишуться у форматі camelCase та приймають функцію, а не рядок. Вбудовані стилі є об’єктами. Щоб детальніше дізнатися про компроміси, які лежать в основі цієї синтаксису, перегляньте чому JSX — це не HTML.
Вбудовування виразів JavaScript
Круглі дужки відкривають можливості JavaScript. Усе, що може бути перетворене на значення, може знаходитися всередині: змінні, виклики функцій, тернарні оператори, методи масивів. У цьому прикладі спочатку обчислюється онлайн-статус:
function UserCard({ user }) {
const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;
А потім кілька виразів формують маркап: скорочений біографічний опис, умовна назва класу, умовна мітка та сформатована дата:
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
<span className={isOnline ? "badge-green" : "badge-grey"}>
{isOnline ? "Online" : "Offline"}
</span>
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}
Правило просте: дужки використовуються для об’єднання виразів, а не операторів. Можна використовувати тернарний оператор, але не блок if, а також .map(), але не цикл for, безпосередньо всередині маркапу.
Компоненти: функції, які повертають інтерфейс
Компонент — це повторно використовуваний, самодостатній елемент інтерфейсу, написаний у вигляді функції JavaScript, яка повертає JSX. Ось і вся визначення:
// A component is just a function that returns JSX
function Button() {
return (
<button className="btn">
Click me
</button>
);
}
Ви використовуєте його так само, як HTML-тег, і можете розмістити стільки, скільки бажаєте:
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
Це відображає три ідентичні кнопки з однієї визначення.
Складання малих компонентів у більші
Компоненти стають потужнішими, коли вони вкладені один у одного. Маленькі, однозадачові елементи об’єднуються у більші, наче будівельні блоки. Аватар знає лише, як відображати зображення:
function Avatar({ src, alt }) {
return <img className="avatar" src={src} alt={alt} />;
}
І на його основі будується ланцюг трохи більших компонентів: блок імені, рядок з інформацією про користувача, який поєднує аватар та ім’я, та картка повідомлення, яка поєднує інформацію про користувача з тілом повідомлення:
function UserName({ name, handle }) {
return (
<div>
<strong>{name}</strong>
<span>@{handle}</span>
</div>
);
}function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} handle={user.handle} />
</div>
);
}function PostCard({ post, author }) {
return (
<article className="post-card">
<UserInfo user={author} />
<p>{post.content}</p>
<span>{post.likes} likes</span>
</article>
);
}
Кожен шар виконує одну функцію. Avatar відображає зображення, UserInfo розміщує аватар поруч із іменем, PostCard структурує ціле повідомлення. Саме таку практику заохочує React: розділяти інтерфейс на найменші зрозумілі частини, надавати кожній чітку функцію та складати їх разом. посібник зі створення повторно використовуваних компонентів React детальніше розглядає проектування компонентів.
Чому назви компонентів пишуться з великої літери
React розрізняє нативні елементи та компоненти за першою літерою. <button> стає звичайним елементом DOM button; <Button> змушує React шукати у межах контексту змінну під назвою Button та викликати її. Компонент, названий малою літерою, потайки розглядається як невідомий HTML-тег, що є поширеною причиною плутанини типу „мій компонент нічого не відображає“.
Props: налаштування компонента ззовні
Кнопка, яка завжди показує текст „Натисніть мене“, не дуже корисна. Props (скорочення від properties) — це спосіб, яким батьківський компонент передає дані дочірньому, тож одна визначення може підходити для багатьох ситуацій.
Props передаються лише в одному напрямку — від компонента, який відображає інший, до того, що саме відображається, і надходять у вигляді одного об’єкта-аргумента. Батьківський компонент записує їх як атрибути:
// Parent passes data as props (looks like HTML attributes)
function App() {
return (
<div>
<Button label="Submit" colour="blue" />
<Button label="Cancel" colour="grey" />
<Button label="Delete" colour="red" />
</div>
);
}
А дочірній елемент розбирає те, що йому потрібно:
// Child receives them as an object
function Button({ label, colour }) {
return (
<button className={`btn btn-${colour}`}>
{label}
</button>
);
}
Одне визначення, три кнопки з різним виглядом, оскільки кожна отримала інші значення.
Пропси можуть містити будь-які значення
Рядки — це лише початок. Числа, логічні значення, масиви, об’єкти та функції — усе це є допустимими пропсами. Наприклад, картка продукту може містити об’єкт даних, функцію-відповідь та флаг:
function ProductCard({ product, onAddToCart, featured }) {
return (
<div className={`card ${featured ? "card-featured" : ""}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<p>{product.rating} ★ ({product.reviewCount} reviews)</p>
<button onClick={onAddToCart}>
Add to Cart
</button>
</div>
);
}
У місці виклику ці значення передаються за допомогою дужок:
// Usage:
<ProductCard
product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
onAddToCart={() => addToCart(product.id)}
featured={true}
/>
Зауважте, що вбудований калебек onAddToCart посилається на product.id, але product тут — це лише об’єкт, переданий як властивість, а не змінна у межах області видимості. У реальному коді слід використовувати змінну, визначену у батьківському компоненті, наприклад onAddToCart={() => addToCart(item.id)}. Передача функцій як властивостей є стандартним способом для дочірніх компонентів повідомляти про події, що знову згадуватиметься нижче.
Властивості є тільки для читання
Компонент ніколи не повинен змінювати власні властивості. Протягом усього процесу відображення вони є фіксованими параметрами. Якщо щось потрібно змінити, батьківський компонент це робить та відображає дочірній компонент із новим значенням. Переатрибування властивості всередині дочірнього компонента є помилкою:
// WRONG — never modify props
function Button({ count }) {
count = count + 1; // ← this is a mistake
return <button>{count}</button>;
}
Перепризначення змінює лише локальну змінну; воно ніколи не доходить до батьківського елемента та зникає під час наступного відображення. Коли компоненту потрібна змінна, яку можна оновлювати, саме для цього існує стан:
// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Тут initialCount лише ініціалізує стан; після першого відображення компонент сам керує своїм підрахунком.
Стан: власна пам’ять компонента
Пропси надходять ззовні. Стан — це дані, якими володіє компонент та які можна змінювати з часом. Коли стан змінюється, React знову відображає компонент із новим значенням, і вам не потрібно безпосередньо маніпулювати DOM.
Стан створюється за допомогою гука useState, імпортованого з React:
import { useState } from "react";
Лічильник демонструє базову структуру:
function Counter() {
const [count, setCount] = useState(0);
// ↑ current value ↑ function to update it ↑ initial value return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useState(0) повертає пару: поточне значення (count, яке починається з 0) та функцію для його зміни (setCount). Виклик цієї функції з новим значенням повідомляє React про необхідність його збереження та переробки компонента.
Побудова кнопки „Лайк“ з використанням стану
Інструктивна кнопка „Лайк“ з початку стає значно простішою. Почнемо з імпорту:
import { useState } from "react";
а потім описайте вигляд кнопки для будь-якої комбінації liked та count:
function LikeButton({ initialLikes }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(initialLikes); function handleClick() {
if (liked) {
setLiked(false);
setCount(c => c - 1);
} else {
setLiked(true);
setCount(c => c + 1);
}
} return (
<button
onClick={handleClick}
className={liked ? "btn-liked" : "btn-default"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
}
Немає пошуку елементів, жодних викликів classList та призначень textContent. Обробник оновлює два значення стану, а JSX визначає, як виглядає кнопка для цих значень. React приводить DOM у відповідність.
Зверніть увагу на форму оновлення setCount(c => c - 1). Передача функції замість значення дає вам найновіший стан, що є безпечнішим, коли кілька оновлень чергуються в одній події.
Кожен екземпляр зберігає власний стан
Стан належить конкретному екземпляру компонента, а не до визначення компонента. Відобразіть три подібні кнопки — кожна з них матиме власні значення liked та count; натискання на одну не впливає на інші:
function Feed() {
return (
<div>
<LikeButton initialLikes={24} /> {/* has its own state */}
<LikeButton initialLikes={7} /> {/* has its own state */}
<LikeButton initialLikes={156} /> {/* has its own state */}
</div>
);
}
Що має бути у стані
Використовуйте стан для значень, які:
- змінюються з часом через взаємодію або надходження даних
- мають оновлювати інтерфейс користувача при зміні
- належать саме цьому екземпляру компонента
Уникайте зберігання в стані всього того, що можна обчислити з існуючих параметрів чи стану (обчислюйте це під час відображення), а також значень, які змінюються без необхідності повторного відображення, наприклад ідентифікатора таймера, який краще зберігати у ref.
Як працює повторне відображення
Повторне відображення є основою всієї моделі. Коли змінюється стан, React знову викликає функцію вашого компонента, отримує новий опис інтерфейсу, порівнює його з попереднім та оновлює лише ті частини DOM, які відрізняються.
Під час першого відображення лічильник створює свою маркувальну структуру, і React формує вузли DOM:
Initial render:
count = 0
Component runs → returns <p>Count: 0</p> <button>Increment</button>
React creates DOM nodes
Коли користувач натискає, функція-змінник ініціює нове відображення, і React порівнює два описи:
User clicks Increment:
setCount(1) called
React re-renders the component
count = 1
Component runs again → returns <p>Count: 1</p> <button>Increment</button>
React compares: <p>Count: 0</p> vs <p>Count: 1</p>
React updates only the text node inside <p>
Button is unchanged — React leaves it alone
У справжньому DOM змінюється лише текст всередині абзацу. Кнопка залишається недоторканою. React не перебудовує сторінку; він обчислює найменший набір змін та застосовує саме їх, тому часті перерендеринги зазвичай не є проблемою.
Що спричиняє перерендеринг компонента
Компонент перерендерується знову, коли змінюється його власний стан:
1. The component's own state changes (setCount, setUser, etc.)
↓
Component re-renders
Він також перерендерується ще в кількох ситуаціях:
2. The component's props change (parent passes different values)
↓
Component re-renders3. A context the component uses changes
↓
Component re-renders4. The parent re-renders
↓
All children re-render (unless memoized)
На практиці „змінилися props“ та „батьківський компонент перерендерувався“ — це однакові події, сприйняті з двох точок зору: дитинний компонент отримує нові props лише тоді, коли його батьківський компонент знову перерендерується. Практичний наслідок полягає у тому, що за замовчуванням перерендеринг батьківського компонента поширюється на всі його дочірні компоненти, незалежно від того, змінилися у них props чи ні, якщо тільки вони не є мемоїзованими.
Ця каскадна обробка рідко становить проблему. Отримання зображення — це просто виклик функції, який створює об’єкти; складною є частина, пов’язана з роботою з справжнім DOM, яку React намагається звести до мінімуму. Більшість справжніх проблем з продуктивністю виникають через структуру компонентів та стану, а не через кількість самої обробки.
Віртуальний DOM та процес узгодження
React зберігає легку JavaScript-репрезентацію інтерфейсу користувача, яку часто називають віртуальним DOM. Кожна обробка створює нову ієрархію об’єктів елементів, і React порівнює її з попередньою ієрархією у процесі, який називається узгодженням. Результатом цього порівняння є список операцій над справжнім DOM, які потрібно виконати.
Ви ніколи не працюєте безпосередньо з цим шаром; це деталь реалізації. Це важливо тому, що порівняння звичайних об’єктів у пам’яті є дешевим, тоді як справжні операції DOM є відносно повільними. Перенаправлення кожної зміни через таке порівняння дозволяє React групувати та мінімізувати дорогі операції.
Декларативний vs імперативний UI
React є декларативним, і розуміння цього поняття — справжня зміна способу мислення. Порівняйте два способи відображення списку.
Імперативний: перелік кроків
У імперативній версії спочатку спорожнюється контейнер:
// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = ""; // clear it
потім кожен елемент створюється, налаштовується та додається вручну:
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item.name;
li.className = item.active ? "active" : "";
li.addEventListener("click", () => handleClick(item.id));
list.appendChild(li);
});
Код — це послідовність команд: створити цей вузол, встановити ту властивість, приєднати цей слухач, вставити його туди. Ви несете відповідальність за все, що відбувається до, після та на кожному з етапів.
Декларативний: опис результату
Декларативна версія визначає, як має виглядати список для будь-якого масиву items:
// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={item.active ? "active" : ""}
onClick={() => onItemClick(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
Немає концепції „очистити список, а потім його відновити“. Ви описуєте бажаний результат, а React сам визначає, як досягти його з поточного DOM. Атрибут key повідомляє React, який елемент є яким під час кожного оновлення, щоб він міг переміщувати, оновлювати або видаляти правильні елементи замість того, щоб створювати їх усі заново.
Чому декларативний стиль підходить для інтерфейсів
- Прогнозованість. За однакових атрибутів та стану компонент виводить однаковий результат. Інтерфейс можна розглядати як чисту функцію ваших даних:
UI = f(state, props). - Менше чого потрібно пам’ятати. Вам не доводиться стежити за тим, як виглядає DOM наразі чи які зміни йому потрібні. Ви просто описуєте те, що є зараз.
Дерево компонентів та однонапрямковий потік даних
Кожен React-додаток є деревом. Кореневий компонент, зазвичай App, відображає своїх дочірніх компонентів, які у свою чергу відображають ще більш дочірніх, відтворюючи структуру екрана:
App
├── Navbar
│ ├── Logo
│ ├── NavLinks
│ └── UserMenu
│ ├── Avatar
│ └── DropdownMenu
├── Dashboard
│ ├── Sidebar
│ │ ├── SidebarLink (×5)
│ │ └── UserStats
│ └── MainContent
│ ├── StatsRow
│ │ ├── StatCard (×4)
│ └── RecentActivity
│ ├── ActivityItem (×10)
└── Footer
Потік даних вниз
Дані передаються від батьківського до дочірнього компонента через пропси. Компонент не може отримати доступ до стану свого братського чи батьківського компонента. Саме цей однонапрямковий потік дозволяє зберігати великі додатки зрозумілими:
App (has user, notifications, theme)
│
├── Navbar (receives: user, notifications)
│ │
│ └── UserMenu (receives: user)
│ │
│ └── Avatar (receives: user.avatar, user.name)
│
└── Dashboard (receives: user, theme)
│
└── MainContent (receives: theme)
Avatar бачить лише те, що передає йому UserMenu, а UserMenu — лише те, що передає йому Navbar. Нічого не витікає ні вбік, ні догори, тож коли значення є неправильним, потрібно перевірити ланцюжок батьківських компонентів.
Потік подій догору
Компонент, розташований глибоко в ієрархії, не може безпосередньо змінювати стан свого батька, але може викликати функцію, яку передав батько. Стан належить батькові:
// Parent owns the state and passes down both the value and the updater
function App() {
const [searchQuery, setSearchQuery] = useState("");
і передає як значення, так і функцію-змінник своїм дочірнім компонентам; пошукова строка викликає цю функцію-змінник щоразу, коли змінюється введений текст:
return (
<div>
<SearchBar
query={searchQuery}
onChange={setSearchQuery} {/* passes the setter as a prop */}
/>
<Results query={searchQuery} />
</div>
);
}// Child receives the updater and calls it on user input
function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={e => onChange(e.target.value)} {/* calls parent's setter */}
placeholder="Search..."
/>
);
}
Запит існує лише в App. SearchBar нічого не зберігає; він повідомляє про кожен натискання клавіші через властивість onChange. Results отримує оновлений запит як властивість та переробляє відображення. Дані надходять у вигляді властивостей, а події — у вигляді викликів функцій. (Пояснювальні коментарі всередині вхідних тегів призначені лише для читання; у справжньому JSX коментар з дужками має знаходитися серед дочірніх елементів, а всередині тега слід використовувати звичайний коментар /* */ або взагалі його пропустити.)
Помилки, які порушують оновлення в React
Зміна стану безпосередньо
Є спокуса безпосередньо змінювати об’єкт стану та передавати його назад:
// WRONG — mutating state directly
const [user, setUser] = useState({ name: "Priya", age: 28 });
Перший приклад birthday змінює об’єкт та передає setterу ту саму посилання, тому інтерфейс не оновлюється. Другий приклад створює новий об’єкт за допомогою оператора розповсюдження, що React сприймає як зміну:
function birthday() {
user.age = 29; // ← directly modifying the object
setUser(user); // ← same object reference — React sees no change
} // UI does NOT update// CORRECT — create a new object
function birthday() {
setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
}
React вирішує, чи змінився стан, порівнюючи посилання за допомогою Object.is, а не перевіряючи їхній вміст. Якщо змінити об’єкт чи масив на місці та передати назад те саме посилання, React прийде до висновку, що нічого не сталося, і може пропустити процес відображення.
Для масивів діє те саме правило. Спроба додати елемент у вже існуючий масив провалюється:
// WRONG — mutating the array
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // ← modifies in place
setItems(items); // ← same reference — no re-render
Тоді як розтягування в новий масив працює:
// CORRECT — create a new array
setItems([...items, 4]); // ← spread creates a new array
Плутанина між пропсами та станом
Швидкий тест допомагає визначити, що є чим. Чи походить значення від батьківського компонента? Це проп, який є лише для читання. Чи належить компоненту це значення та він змінює його з часом? Це стан. Блокування значення, яке ніколи не змінюється, у функції useState — ознака плутанини:
// Wrong: using state for something that should be a prop
function UserCard() {
const [userName] = useState("Priya"); // ← why is this state? It never changes here
return <p>{userName}</p>;
}
Якщо батьківський компонент контролює дані, прийміть їх як проп:
// Correct: static data the parent controls comes as a prop
function UserCard({ name }) {
return <p>{name}</p>;
}
Зберігання значень, які можна обчислити
Не кожна зміна потребує власного стану. Зберігання підрахунку поруч із списком, який він підраховує, призводить до дублювання інформації:
// Wrong: storing derived data in state
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // ← why is this state?
Тепер кожна зміна у items вимагає відповідного оновлення itemCount, а одне забуте оновлення призводить до їхньої неузгодженості. Обчислення цього показника під час відображення за своєю суттю підтримує їх у синхронності:
// Every time items changes, you have to remember to also update itemCount
// And if you forget once, they're out of sync// Correct: derive it during render
const [items, setItems] = useState([...]);
const itemCount = items.length; // ← just a variable — always in sync
Компоненти, які роблять усе
Один компонент, який відображає бічну панель, таблицю, форму та кілька модалних вікон, є складним для читання, тестування та повторного використання. Корисна гіпотеза: якщо ви не можете описати функції компонента за одним коротким реченням, він робить занадто багато.
// Hard to maintain — does everything
function UserDashboard() {
// manages user state
// fetches orders
// handles filters
// manages pagination
// controls modal open/close
// renders sidebar
// renders order table
// renders filter controls
// renders pagination
// renders modal
return ( /* 200 lines of JSX */ );
}
Розділення його за функціями надає кожній частині ім’я та завдання:
// Better — clear single responsibilities
function UserDashboard() {
return (
<DashboardLayout>
<OrderFilters />
<OrderTable />
<OrderPagination />
<OrderDetailModal />
</DashboardLayout>
);
}
Проектування додатку з використанням компонентів
Складіть схему меж перед написанням коду
Почніть з дизайну та позначте окремі елементи. Усе, що повторюється, є компонентом. Усе, що виконує одну чітку функцію, є компонентом. Елементи, які змінюються разом, мають бути об’єднані; елементи, які змінюються незалежно, слід розділити. Для сторінки опису продукту дизайн:
Looking at the design:
[ Filter Bar ]
[ Product Card ][ Product Card ][ Product Card ]
[ Product Card ][ Product Card ][ Product Card ]
[ Pagination ]
розділяється на таку структуру:
Components:
ProductPage
├── FilterBar
│ ├── FilterGroup (×3)
│ └── SortDropdown
├── ProductGrid
│ └── ProductCard (×N)
└── Pagination
Розмістіть стан у найнижчому спільному батькові
Для кожного елемента стану знайдіть у дереві найнижчий компонент, який його потребує, та зберігайте стан саме там:
If only ProductCard needs the "expanded" state → put it in ProductCard
If FilterBar and ProductGrid both need filters → put filters in ProductPage
If Pagination and ProductGrid both need currentPage → put it in ProductPage
Коли два сусідні елементи потребують одних і тих самих даних, перемістіть стан у їхнього найближчого спільного батька та передавайте його далі. Це називається піднесенням стану. Зберігання стану на якомога нижчому рівні також обмежує кількість елементів дерева, які переробляються під час його зміни.
Спочатку створіть статичну версію, потім додайте стан
Почніть з жорстко заданих даних: без useState, без обробників подій, лише маркапт, керований пропсами. Коли структура буде визначена, з’ясуйте, які значення дійсно змінюються з часом, та введіть стан саме для них. Перший крок — це повністю статична картка:
// Step 1: static — no state, hardcoded data
function ProductCard() {
return (
<div className="card">
<img src="/headphones.jpg" alt="Headphones" />
<h3>Wireless Headphones</h3>
<p>₹2,499</p>
<button>Add to Cart</button>
</div>
);
}
Другий крок замінює жорстко заданий контент на пропс product, а третій додає невеликий фрагмент стану для підтвердження „Додано“, який себе скасовує через дві секунди:
// Step 2: accept props — still no state
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button>Add to Cart</button>
</div>
);
}// Step 3: add state for interactivity
function ProductCard({ product, onAddToCart }) {
const [added, setAdded] = useState(false); function handleAdd() {
setAdded(true);
onAddToCart(product.id);
setTimeout(() => setAdded(false), 2000);
} return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button onClick={handleAdd} disabled={added}>
{added ? "Added ✓" : "Add to Cart"}
</button>
</div>
);
}
Оскільки структура та інтерактивність розділені, кожен крок легко перевірити окремо.
Ключові висновки
Уся модель у стислому вигляді. Чому існує React:
Why React:
Plain JS DOM manipulation is hard to scale and maintain
React: describe the UI, let React handle DOM updates
та основи кожної з вищезгаданих концепцій, від JSX до поширених помилок:
JSX:
HTML-like syntax in JavaScript — compiled to React.createElement()
{} embeds any JavaScript expression
Use className (not class), onClick (not onclick)Components:
Functions that return JSX
Capital letter names (Button, not button)
Reusable, composable building blocksProps:
Data passed from parent to child (like function arguments)
Read-only — a component never modifies its own props
Can be strings, numbers, objects, arrays, functionsState:
const [value, setValue] = useState(initialValue)
Data owned by a component that changes over time
Calling the setter triggers a re-render
Each component instance has its own stateRe-rendering:
Happens when state changes, props change, or parent re-renders
React diffs the virtual DOM and updates only what changed
Not expensive — surgical DOM updatesDeclarative:
Describe what the UI should look like
React figures out what changed and how to update the DOM
UI = f(state, props) — predictable, testableData flow:
Props flow down (parent → child)
Events flow up (child calls parent's function)
One-way data flow keeps the application predictableCommon mistakes:
Mutating state directly (use spread / new objects)
Storing derived values in state (compute them during render)
Overloading one component (split by responsibility)
- Компоненти — це функції, пропси — їхні аргументи, а стан — їхня пам’ять. UI завжди є проекцією поточного стану.
- Повторне відрендерування за своєю суттю є дешевим; складною частиною є обробка DOM, яку заощаджує React. Спочатку належним чином організуйте стан, перш ніж турбуватися про кількість відрендерувань.
- Завжди передавайте у функції-змінники нові об’єкти та масиви; React порівнює посилання, а не вміст.
- Зберігайте стан у найнижчому компоненті, який його потребує, отримуйте все інше під час відрендерування та дозвольте подіям проходити вгору через пропси-функції.
Контекст, ефекти, власні хуки та налаштування продуктивності ґрунтуються на цих принципах. Якщо ви добре розумієте компоненти, пропси, стан та процес повторного відрендерування, більш складні аспекти React стають розширенням моделі, яку ви вже знаєте, а не новими правилами, які потрібно запам’ятати.
Пов’язана література
- Як уникнути проблем зі станом через мутації посилань у JavaScript — Дізнайтеся, чому мутації об’єктів та масивів через посилання порушують процес перерендерингу в React, чому функція spread копіює дані лише поверхнево, та як безпечно створювати глибокі клони стану.
- Розуміння користувацьких гаків у React: повторне використання логіки без спільного стану — Дізнайтеся, що таке користувацькі гаки у React, як вони дозволяють екстрагувати та поширювати логіку, що залежить від стану, між компонентами, та які поширені помилки варто уникати під час їх створення.