JSX та TSX — синтаксичний цукор, а не магія
JSX та TSX — синтаксичний цукор, а не магія
🎯 Мета лекції
- Зрозуміти природу JSX: це синтаксичне розширення JavaScript, а не окрема мова чи магія.
- Опанувати процес трансформації JSX → JavaScript через Babel та esbuild.
- Розібрати різницю між класичним
React.createElementта сучаснимjsx-runtime. - Усвідомити ключові відмінності між HTML та JSX синтаксисом.
- Навчитися правильно використовувати TypeScript з TSX для типобезпечного коду.
🔑 Ключові терміни
- JSX (JavaScript XML) — синтаксичне розширення JavaScript для декларативного опису UI.
- TSX (TypeScript JSX) — JSX із підтримкою статичної типізації TypeScript.
- Транспіляція (transpilation) — перетворення коду з однієї форми JavaScript у іншу (JSX → звичайний JS).
- React Element — результат виклику
React.createElement()або_jsx(): незмінний JavaScript-об'єкт. - jsx-runtime — сучасний трансформ JSX (React 17+), що не вимагає імпорту
React.
🔗 Офіційний референс React:
Написання розмітки з JSX · JavaScript в JSX з фігурними дужками
Фундаментальне питання: що таке JSX насправді?
Перш ніж ви почнете писати React-компоненти, критично важливо усвідомити одну фундаментальну істину: JSX — це не HTML, не шаблонна мова, не магічний синтаксис браузера. JSX — це синтаксичний цукор (syntactic sugar) для виклику функцій JavaScript.
Коли ви пишете:
const element = <h1 className="title">Привіт, світ!</h1>;
Це не є валідний JavaScript. Браузер не розуміє цього синтаксису. Це розширення, яке потребує попередньої обробки — трансформації у звичайний JavaScript.
- Arrow functions у JavaScript:
(x) => x * 2→function(x) { return x * 2; } - Template literals:
`Hello, ${name}`→"Hello, " + name - Async/await:
await promise→promise.then(...) - JSX:
<div>Text</div>→React.createElement("div", null, "Text")
Від JSX до JavaScript: покрокова трансформація
Розберемо, що відбувається з JSX на всіх етапах.
Крок 1: Вихідний JSX-код
// src/App.jsx — це те, що ви пишете
const element = <h1 className="title">Привіт, світ!</h1>;
Крок 2: Транспіляція через Babel або esbuild
Інструмент збірки (Vite використовує esbuild) перетворює JSX у виклик функції:
// Класичний трансформ (React 16 і раніше)
const element = React.createElement(
"h1", // type: тип елемента (рядок для HTML-тегів)
{ className: "title" }, // props: об'єкт властивостей
"Привіт, світ!" // children: дочірні елементи
);
Крок 3: Виконання у браузері
Функція React.createElement() повертає React Element — звичайний JavaScript-об'єкт:
// Результат виконання React.createElement
const element = {
type: "h1",
props: {
className: "title",
children: "Привіт, світ!"
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
};
Крок 4: React перетворює об'єкт у реальний DOM
React читає цей об'єкт та викликає DOM API:
// Приблизно те, що робить React (спрощено)
const domNode = document.createElement("h1");
domNode.className = "title";
domNode.textContent = "Привіт, світ!";
parentElement.appendChild(domNode);
React.createElement(). Ви могли б писати React.createElement() вручну, але JSX робить код набагато читабельнішим:// Без JSX — важко читати
React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Заголовок"),
React.createElement("p", null, "Параграф")
)
// З JSX — природна HTML-подібна структура
<div className="container">
<h1>Заголовок</h1>
<p>Параграф</p>
</div>
Класичний трансформ проти сучасного jsx-runtime
До React 17 єдиним способом трансформації JSX був виклик React.createElement(). Це вимагало наявності React у scope кожного файлу з JSX, навіть якщо ви його явно не використовували:
// До React 17 — обов'язковий імпорт!
import React from 'react';
function App() {
return <div>Привіт!</div>;
}
Якщо ви забували імпортувати React, отримували помилку:
ReferenceError: React is not defined
Сучасний трансформ (React 17+, jsx-runtime)
React 17 представив новий трансформ під назвою jsx-runtime, який не вимагає явного імпорту React. Компілятор автоматично імпортує спеціальні функції з react/jsx-runtime:
// React 17+ — імпорт React не потрібен!
function App() {
return <div>Привіт!</div>;
}
Що відбувається під капотом:
// Автоматичний імпорт (додається компілятором, ви його не бачите)
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx("div", { children: "Привіт!" });
}
Переваги нового трансформу:
- Менше бойлерплейту: Не потрібно імпортувати
Reactу кожному файлі. - Менший розмір бандлу: Якщо файл містить лише JSX без хуків або інших React API, імпорт
Reactне додається. - Швидша компіляція: Функції
_jsx/_jsxsоптимізовані для швидкості.
tsconfig.json (або jsconfig.json для JavaScript-проєктів) встановіть:{
"compilerOptions": {
"jsx": "react-jsx" // Сучасний трансформ (React 17+)
}
}
"jsx": "react"— класичний трансформ (React.createElement), вимагаєimport React."jsx": "react-jsxdev"— режим розробки з додатковими перевірками."jsx": "preserve"— не трансформувати JSX (для інших інструментів, наприклад Babel).
create-vite із react-ts автоматично встановлює "react-jsx".HTML проти JSX: критичні відмінності
JSX схожий на HTML, але це не HTML. Є кілька ключових відмінностей, які призводять до помилок у початківців.
Відмінність 1: className замість class
У HTML:
<div class="container">Контент</div>
У JSX:
<div className="container">Контент</div>
Чому?
class — це зарезервоване слово у JavaScript (використовується для оголошення класів: class MyClass {}). JSX — це JavaScript, тому потрібно використовувати альтернативну назву, що відповідає DOM API: element.className.
// ❌ НЕПРАВИЛЬНО
<div class="box">Текст</div>
// ✅ ПРАВИЛЬНО
<div className="box">Текст</div>
Warning: Invalid DOM property `class`. Did you mean `className`?
Відмінність 2: htmlFor замість for
У HTML:
<label for="username">Ім'я користувача:</label>
<input id="username" type="text" />
У JSX:
<label htmlFor="username">Ім'я користувача:</label>
<input id="username" type="text" />
Чому?
for — це зарезервоване слово у JavaScript (цикл for). DOM API використовує властивість htmlFor, тому JSX слідує цій конвенції.
Відмінність 3: camelCase для DOM-властивостей та обробників подій
У HTML атрибути записуються у lowercase або kebab-case:
<button onclick="handleClick()">Натисни</button>
<div tabindex="0">Фокусований елемент</div>
У JSX використовується camelCase для DOM-властивостей та обробників подій:
<button onClick={handleClick}>Натисни</button>
<div tabIndex={0}>Фокусований елемент</div>
Чому?
Це відповідає JavaScript DOM API:
element.onclick = handleClick; // HTML атрибут
element.addEventListener('click'); // Але обробник у camelCase
element.tabIndex = 0; // DOM-властивість
JSX слідує JavaScript-конвенціям, а не HTML.
Список найпоширеніших перетворень:
| HTML атрибут | JSX властивість |
|---|---|
onclick | onClick |
onchange | onChange |
onfocus | onFocus |
onblur | onBlur |
onsubmit | onSubmit |
tabindex | tabIndex |
maxlength | maxLength |
readonly | readOnly |
autocomplete | autoComplete |
autofocus | autoFocus |
data-* та aria-* атрибутиАтрибути з префіксами data- та aria- залишаються у kebab-case, як і в HTML:<div
data-user-id="42"
aria-label="Закрити модальне вікно"
>
Контент
</div>
Відмінність 4: style як об'єкт, а не рядок
У HTML style — це рядок:
<div style="color: red; font-size: 16px;">Текст</div>
У JSX style — це JavaScript-об'єкт із camelCase-властивостями:
<div style={{ color: 'red', fontSize: '16px' }}>Текст</div>
// ↑ Подвійні фігурні дужки!
Розбір подвійних фігурних дужок:
- Зовнішні
{}— вхід у JavaScript-вираз всередині JSX. - Внутрішні
{}— літерал JavaScript-об'єкта.
// Покроково
const styleObject = { color: 'red', fontSize: '16px' };
<div style={styleObject}>Текст</div>
// Інлайн (найпоширеніший спосіб)
<div style={{ color: 'red', fontSize: '16px' }}>Текст</div>
Властивості CSS у camelCase:
| CSS-властивість | JSX style-об'єкт |
|---|---|
background-color | backgroundColor |
font-size | fontSize |
margin-top | marginTop |
z-index | zIndex |
border-radius | borderRadius |
Числові значення (пікселі):
React автоматично додає px до числових значень певних властивостей:
<div style={{ width: 200, height: 100 }}>
// Перетвориться на: width: 200px; height: 100px;
</div>
Для інших одиниць використовуйте рядки:
<div style={{ width: '50%', height: '10rem', marginTop: '2em' }}>
Контент
</div>
// ❌ НЕПРАВИЛЬНО — style як рядок
<div style="color: red;">Текст</div>
// ❌ НЕПРАВИЛЬНО — kebab-case властивості
<div style={{ 'font-size': '16px' }}>Текст</div>
// ✅ ПРАВИЛЬНО
<div style={{ fontSize: '16px', color: 'red' }}>Текст</div>
Відмінність 5: Усі теги мають бути закритими
У HTML деякі теги можуть бути самозакривними (self-closing) без слеша:
<!-- HTML — допускається -->
<img src="logo.png">
<input type="text">
<br>
<hr>
У JSX всі теги обов'язково закриваються:
{/* JSX — обов'язковий слеш або закриваючий тег */}
<img src="logo.png" />
<input type="text" />
<br />
<hr />
Чому?
JSX базується на XML-синтаксисі, де кожен тег повинен мати чітке завершення. Це усуває двозначність парсингу.
// Рекомендований стиль
<img src="logo.png" />
<MyComponent />
// Працює, але менш читабельно
<img src="logo.png"/>
<MyComponent/>
Відмінність 6: Коментарі у JSX
У HTML:
<!-- Це HTML-коментар -->
<div>Контент</div>
У JSX коментарі всередині розмітки повинні бути JavaScript-коментарями у фігурних дужках:
{/* Це JSX-коментар */}
<div>Контент</div>
{/*
Багаторядковий
коментар у JSX
*/}
<div>Інший контент</div>
Чому не працює <!-- -->?
JSX транспілюється у JavaScript. Конструкція <!-- --> не є валідним JavaScript-синтаксисом. Тому потрібно використовувати JavaScript-коментарі /* */ всередині фігурних дужок {}.
Фігурні дужки {} — вхід у JavaScript-світ
Усередині JSX ви можете вставляти будь-які JavaScript-вирази через фігурні дужки {}.
Що можна вставляти через {}
1. Змінні:
const name = "Олександр";
const element = <h1>Привіт, {name}!</h1>;
2. Виклики функцій:
function getGreeting(name) {
return `Вітаємо, ${name}`;
}
const element = <h1>{getGreeting("Марія")}</h1>;
3. Вирази (expressions):
const a = 5;
const b = 10;
const element = <p>Сума: {a + b}</p>; // Виведе: Сума: 15
4. Тернарний оператор:
const isLoggedIn = true;
const element = (
<div>
{isLoggedIn ? <p>Ласкаво просимо!</p> : <p>Будь ласка, увійдіть.</p>}
</div>
);
5. Логічні оператори (&&, ||):
const hasNewMessages = true;
const messageCount = 5;
const element = (
<div>
{hasNewMessages && <span>У вас {messageCount} нових повідомлень</span>}
</div>
);
6. Масиви (рендеринг списків):
const items = ['Яблуко', 'Банан', 'Апельсин'];
const element = (
<ul>
{items.map((item, index) => <li key={index}>{item}</li>)}
</ul>
);
Що НЕ можна вставляти через {}
Інструкції (statements) не є виразами:
// ❌ НЕПРАВИЛЬНО — if є інструкцією, а не виразом
<div>
{if (isLoggedIn) { return <p>Привіт!</p>; }}
</div>
// ✅ ПРАВИЛЬНО — тернарний оператор є виразом
<div>
{isLoggedIn ? <p>Привіт!</p> : null}
</div>
Цикли for, while не працюють:
// ❌ НЕПРАВИЛЬНО
<ul>
{for (let i = 0; i < 5; i++) { <li>{i}</li> }}
</ul>
// ✅ ПРАВИЛЬНО — використовуйте .map()
<ul>
{[0, 1, 2, 3, 4].map(i => <li key={i}>{i}</li>)}
</ul>
- Вираз (expression) — це фрагмент коду, що повертає значення:
a + b,Math.random(),isActive ? 'так' : 'ні'. - Інструкція (statement) — це команда, яка виконує дію, але не повертає значення:
if (...) {},for (...) {},const x = 5;.
{} у JSX можна використовувати лише вирази.Приклад: умовний рендеринг
Збирання React застосунку…
Один батьківський елемент: правило коренів
JSX-вираз має містити лише один кореневий елемент. Це означає, що ви не можете повернути кілька сусідніх елементів без загальної обгортки.
Проблемний код:
// ❌ НЕПРАВИЛЬНО — два кореневі елементи
function App() {
return (
<h1>Заголовок</h1>
<p>Параграф</p>
);
}
Помилка компіляції:
Adjacent JSX elements must be wrapped in an enclosing tag.
Рішення 1: Обгорнути у <div>
// ✅ Працює, але додає зайвий DOM-елемент
function App() {
return (
<div>
<h1>Заголовок</h1>
<p>Параграф</p>
</div>
);
}
Проблема: У DOM з'явиться зайвий <div>, що може вплинути на CSS-стилі або семантику.
Рішення 2: Використати React Fragment
React Fragment (<React.Fragment> або скорочений синтаксис <>...</>) — це спеціальний компонент, що дозволяє групувати елементи без створення додаткового DOM-вузла.
// ✅ ПРАВИЛЬНО — Fragment не створює DOM-елемент
import { Fragment } from 'react';
function App() {
return (
<Fragment>
<h1>Заголовок</h1>
<p>Параграф</p>
</Fragment>
);
}
Скорочений синтаксис:
// ✅ Найпоширеніший спосіб
function App() {
return (
<>
<h1>Заголовок</h1>
<p>Параграф</p>
</>
);
}
Результат у DOM:
<!-- У DOM немає обгортки, лише самі елементи -->
<h1>Заголовок</h1>
<p>Параграф</p>
- Для списків без обгортки: Коли потрібно повернути кілька елементів із
.map(). - Для уникнення порушення семантики HTML: Наприклад, у
<table>не можна мати<div>між<tbody>та<tr>. - Для CSS Grid/Flexbox: Зайві обгортки можуть зламати layout.
Fragment із key (для списків):
Скорочений синтаксис <> не підтримує key. Якщо потрібен key (наприклад, у списках), використовуйте повну форму:
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
TSX: типізація JSX у TypeScript
TSX — це розширення файлів .tsx, що поєднує JSX із TypeScript. TypeScript аналізує JSX-код та перевіряє типи пропсів, обробників подій та повернутих значень.
Базова різниця між .jsx та .tsx
Файл .jsx:
// MyButton.jsx — JavaScript + JSX
function MyButton(props) {
return <button onClick={props.onClick}>{props.label}</button>;
}
Файл .tsx:
// MyButton.tsx — TypeScript + JSX
interface MyButtonProps {
label: string;
onClick: () => void;
}
function MyButton(props: MyButtonProps) {
return <button onClick={props.onClick}>{props.label}</button>;
}
Що додає TypeScript:
- Перевірка типів пропсів: Якщо ви передасте
labelяк число замість рядка, отримаєте помилку на етапі компіляції. - Автодоповнення у редакторі: VS Code підказує доступні властивості
props. - Захист від typo: Якщо ви напишете
props.lable(з помилкою), TypeScript покаже помилку.
Типізація компонентів: React.FC проти явних типів
Існують два основні підходи до типізації функціональних компонентів.
Підхід 1: Явна типізація параметрів (рекомендовано)
interface GreetingProps {
name: string;
age?: number; // Опційна властивість
}
function Greeting(props: GreetingProps) {
return (
<div>
<h1>Привіт, {props.name}!</h1>
{props.age && <p>Вік: {props.age}</p>}
</div>
);
}
Підхід 2: Використання React.FC<Props> (застарілий стиль, але все ще зустрічається)
import { FC } from 'react';
interface GreetingProps {
name: string;
age?: number;
}
const Greeting: FC<GreetingProps> = (props) => {
return (
<div>
<h1>Привіт, {props.name}!</h1>
{props.age && <p>Вік: {props.age}</p>}
</div>
);
};
Чому перший підхід кращий:
- Простіший: Не потрібно імпортувати
FCта використовувати generic-синтаксис. - Явність: Тип функції визначається природно через параметри.
- Гнучкість: Легше додавати кілька параметрів або повертати різні типи.
React.FC більше не рекомендуєтьсяДо React 18 React.FC автоматично додавав тип для children (дочірніх елементів). Це призводило до плутанини: компоненти, які не повинні мати children, все одно приймали їх без помилки.З React 18 офіційна документація рекомендує явну типізацію параметрів та явне оголошення children, якщо компонент їх приймає:interface CardProps {
title: string;
children: React.ReactNode; // Явно оголошуємо children
}
function Card(props: CardProps) {
return (
<div className="card">
<h2>{props.title}</h2>
{props.children}
</div>
);
}
Типи для обробників подій
Обробники подій у React мають специфічні типи, що відрізняються від нативних DOM-типів.
Приклад: обробник onClick
import { MouseEvent } from 'react';
function Button() {
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
console.log('Натиснуто кнопку', event.currentTarget);
};
return <button onClick={handleClick}>Натисни мене</button>;
}
Типи для найпоширеніших подій:
| Подія | Тип React |
|---|---|
onClick | MouseEvent<HTMLButtonElement> |
onChange (input) | ChangeEvent<HTMLInputElement> |
onSubmit (form) | FormEvent<HTMLFormElement> |
onFocus | FocusEvent<HTMLInputElement> |
onKeyDown | KeyboardEvent<HTMLInputElement> |
Скорочена форма (без event):
Якщо ви не використовуєте об'єкт event, можна визначити обробник без типу:
function Counter() {
const [count, setCount] = useState(0);
// Тип виводиться автоматично: () => void
const increment = () => setCount(count + 1);
return <button onClick={increment}>Лічильник: {count}</button>;
}
Типізація useState з явним типом
TypeScript часто виводить тип стану автоматично, але іноді потрібна явна типізація:
// Автоматичний вивід типу
const [count, setCount] = useState(0); // count: number
// Явна типізація для складних типів
interface User {
id: number;
name: string;
}
const [user, setUser] = useState<User | null>(null);
// ↑ Явно вказуємо, що може бути null
Коли потрібна явна типізація:
- Union types:
useState<string | null>(null)— стан може бути рядком абоnull. - Складні об'єкти:
useState<User>({ id: 1, name: 'Іван' }). - Масиви:
useState<number[]>([])— порожній масив, TypeScript не може вивести тип елементів.
Збирання React застосунку…
Поширені помилки та підводні камені
Помилка 1: Забути key у списках
// ❌ НЕПРАВИЛЬНО — відсутній key
{items.map(item => <div>{item.name}</div>)}
// ✅ ПРАВИЛЬНО
{items.map(item => <div key={item.id}>{item.name}</div>)}
Чому це важливо:
Без key React не може ефективно відстежувати елементи списку при додаванні, видаленні або зміні порядку. Це призводить до:
- Повільного рендерингу (React перестворює всі елементи замість оновлення змінених).
- Збереження неправильного стану (наприклад, фокус на неправильному елементі).
key// ⚠️ НЕБЕЗПЕЧНО — індекс як key
{items.map((item, index) => <div key={index}>{item.name}</div>)}
key працює лише якщо:- Список ніколи не змінюється (статичні дані).
- Елементи не можуть бути відсортовані або відфільтровані.
- Елементи не мають внутрішнього стану (input-полів, розгорнутих/згорнутих секцій).
id, uuid).Помилка 2: Намагатися змінити props
// ❌ НЕПРАВИЛЬНО — props є read-only
function Button(props: { label: string }) {
props.label = "Змінено"; // Помилка!
return <button>{props.label}</button>;
}
Props — це незмінні (immutable). Спроба змінити їх призведе до помилки у React StrictMode.
Якщо потрібна локальна копія пропса для зміни:
// ✅ ПРАВИЛЬНО — створюємо локальний стан
function EditableLabel(props: { initialValue: string }) {
const [value, setValue] = useState(props.initialValue);
return (
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
Помилка 3: Забути обгорнути JSX у дужки при перенесенні на новий рядок
// ❌ НЕПРАВИЛЬНО — JavaScript вставить крапку з комою після return
function App() {
return
<div>Контент</div>;
}
// Результат: return; <div>Контент</div>; — JSX не виконується!
Правильно:
// ✅ Варіант 1: JSX на одному рядку з return
function App() {
return <div>Контент</div>;
}
// ✅ Варіант 2: Обгорнути у дужки
function App() {
return (
<div>
<h1>Заголовок</h1>
<p>Параграф</p>
</div>
);
}
; після return, якщо на тому ж рядку нічого немає. Дужки () запобігають цьому, сигналізуючи, що вираз продовжується на наступному рядку.Помилка 4: Використання зарезервованих слів JavaScript
// ❌ НЕПРАВИЛЬНО
<label for="username">Ім'я</label>
<div class="container">Контент</div>
// ✅ ПРАВИЛЬНО
<label htmlFor="username">Ім'я</label>
<div className="container">Контент</div>
Помилка 5: Забути закрити самозакривні теги
// ❌ НЕПРАВИЛЬНО
<img src="logo.png">
<input type="text">
// ✅ ПРАВИЛЬНО
<img src="logo.png" />
<input type="text" />
Порівняльна таблиця: HTML проти JSX
| Аспект | HTML | JSX |
|---|---|---|
| Атрибут класу | class="box" | className="box" |
| Label for | for="id" | htmlFor="id" |
| Inline-стилі | style="color: red;" | style={{ color: 'red' }} |
| Обробники подій | onclick="..." (lowercase) | onClick={...} (camelCase) |
| Самозакривні теги | <img> або <img /> | Обов'язково <img /> |
| Коментарі | <!-- коментар --> | {/* коментар */} |
| Вставка змінних | Неможлива напряму | {variable} |
| Кілька кореневих тегів | Допускається | Потрібна обгортка або <Fragment> |
| Логіка у розмітці | Немає (лише статичний HTML) | Вирази JavaScript через {} |
Атрибути data-* / aria-* | kebab-case | kebab-case (виняток) |
Інструменти для роботи з JSX/TSX
Babel REPL — онлайн-транспілятор
Щоб побачити, як JSX перетворюється на JavaScript, використовуйте Babel REPL:
- Відкрийте https://babeljs.io/repl
- Увімкніть пресет
@babel/preset-react - Введіть JSX у ліву панель — побачите результат трансформації праворуч
Приклад:
// Вхід
<div className="container">
<h1>Привіт</h1>
</div>
// Вихід (react-jsx трансформ)
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
_jsxs("div", {
className: "container",
children: [
_jsx("h1", { children: "Привіт" })
]
});
ESLint плагіни для React
Для виявлення поширених помилок у JSX/TSX встановіть ESLint плагіни:
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks
Додайте у .eslintrc.js або eslint.config.js:
{
plugins: ['react', 'react-hooks'],
rules: {
'react/jsx-key': 'error', // Помилка, якщо немає key у списках
'react/jsx-no-duplicate-props': 'error', // Заборона дублікатів пропсів
'react-hooks/rules-of-hooks': 'error', // Правила хуків
}
}
Prettier для автоформатування
Prettier автоматично форматує JSX/TSX:
pnpm add -D prettier
Створіть .prettierrc:
{
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "es5",
"printWidth": 80
}
Prettier автоматично:
- Додає пробіл перед
/>у самозакривних тегах. - Форматує багаторядковий JSX.
- Вирівнює атрибути.
Глибше у трансформацію: різниця між createElement та jsx-runtime
Щоб повністю зрозуміти JSX, розглянемо докладніше, як працюють обидва методи трансформації.
Класичний трансформ: React.createElement
Сигнатура функції:
function createElement(
type: string | ComponentType,
props: object | null,
...children: ReactNode[]
): ReactElement
Приклад трансформації:
// JSX
<div id="root">
<h1>Привіт</h1>
<p>Параграф</p>
</div>
// Трансформація
React.createElement(
"div",
{ id: "root" },
React.createElement("h1", null, "Привіт"),
React.createElement("p", null, "Параграф")
)
Особливості:
- Усі дочірні елементи передаються як окремі аргументи (
...children). - Потрібен
import React from 'react'у кожному файлі. childrenможе бути як уprops.children, так і в окремих аргументах.
Сучасний трансформ: jsx-runtime
Сигнатури функцій:
// Для елементів без дочірніх або з одним дочірнім
function jsx(
type: string | ComponentType,
props: object,
key?: string | number
): ReactElement
// Для елементів з кількома дочірніми
function jsxs(
type: string | ComponentType,
props: object,
key?: string | number
): ReactElement
Приклад трансформації:
// JSX
<div id="root">
<h1>Привіт</h1>
<p>Параграф</p>
</div>
// Трансформація (react-jsx)
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
_jsxs("div", {
id: "root",
children: [
_jsx("h1", { children: "Привіт" }),
_jsx("p", { children: "Параграф" })
]
})
Ключові відмінності:
childrenзавжди уprops: Дочірні елементи передаються якprops.children, а не окремими аргументами.- Розділення
jsxтаjsxs:jsx()— для елементів без дочірніх або з одним дочірнім.jsxs()— для елементів з масивом дочірніх (множинніchildren).
keyяк окремий параметр: Не входить уprops, а передається третім аргументом.- Автоматичний імпорт: Компілятор сам додає імпорт з
react/jsx-runtime.
jsx та jsxs окремо?Це оптимізація продуктивності. Коли React знає, що дочірній елемент один (jsx()), він уникає створення масиву children. Це зменшує навантаження на garbage collector та прискорює рендеринг.Візуалізація різниці
Спеціальні пропси: key та ref
У JSX є два спеціальні пропси, які React обробляє по-особливому: key та ref.
Пропс key
key — це спеціальний атрибут, що допомагає React ідентифікувати елементи у списках. Він не передається як звичайний пропс компоненту.
function TodoList({ items }: { items: Array<{ id: string; text: string }> }) {
return (
<ul>
{items.map(item => (
<TodoItem key={item.id} text={item.text} />
// ↑ key не доступний всередині TodoItem
))}
</ul>
);
}
function TodoItem({ text }: { text: string }) {
// Тут НЕМАЄ доступу до key через props
return <li>{text}</li>;
}
Чому key особливий:
- React використовує
keyдля внутрішнього алгоритму reconciliation. keyне доступний черезprops.keyусередині компонента.- Якщо вам потрібне значення
keyвсередині компонента, передайте його як окремий пропс:
<TodoItem key={item.id} id={item.id} text={item.text} />
// ↑ key для React ↑ id для компонента
Пропс ref
ref — це механізм доступу до реального DOM-елемента або екземпляра класового компонента. Як і key, він не передається через props.
import { useRef } from 'react';
function InputWithFocus() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Фокус на input</button>
</>
);
}
Детальніше про ref ми розглянемо у лекції про хуки.
key та ref особливі?Вони є частиною внутрішнього API React, а не публічного інтерфейсу компонента. React резервує ці імена для своїх потреб і не пропускає їх у props, щоб уникнути конфліктів.Практика: побудова форми з типізацією
Закріпимо знання, створивши форму реєстрації з повною типізацією TypeScript.
Збирання React застосунку…
Ключові моменти прикладу:
- Типізація стану:
useState<FormData>таuseState<FormErrors>забезпечують типобезпеку. - Типізація подій:
FormEvent<HTMLFormElement>таChangeEvent<HTMLInputElement>для обробників. - Валідація форми: Функція
validateForm()повертаєbooleanта оновлює стан помилок. - Умовний рендеринг: Після успішної відправки показується інший компонент.
- Використання
htmlFor: Зв'язок<label>з<input>черезhtmlFor="username".
Підсумок
✅ Що ми опанували
- JSX — це синтаксичний цукор для
React.createElement()або_jsx()ізjsx-runtime. - JSX транспілюється у JavaScript через Babel або esbuild до того, як код потрапляє у браузер.
- HTML ≠ JSX:
className,htmlFor, camelCase для подій,styleяк об'єкт, обов'язкове закриття тегів. - Фігурні дужки
{}дозволяють вставляти JavaScript-вирази, але не інструкції. - Fragment
<>...</>дозволяє групувати елементи без додаткового DOM-вузла.
🔑 Ключові правила
- Завжди використовуйте
keyу списках із унікальним ідентифікатором (не індексом). - Props є read-only — ніколи не змінюйте їх напряму.
- Один кореневий елемент — використовуйте
<Fragment>або обгортку. - TSX типізація: явно вказуйте типи пропсів, подій та стану для типобезпеки.
keyтаref— спеціальні пропси, недоступні черезprops.
Запитання для самоперевірки
Так, JSX — це не ексклюзивна технологія React. Інші фреймворки та бібліотеки теж використовують JSX:
- Preact — легковажна альтернатива React.
- Solid.js — реактивний фреймворк із JSX-синтаксисом.
- Inferno — швидкий React-подібний фреймворк.
Babel можна налаштувати на використання кастомної функції замість React.createElement:
{
"plugins": [
["@babel/plugin-transform-react-jsx", {
"pragma": "h", // Замість React.createElement
"pragmaFrag": "Fragment"
}]
]
}
class — це зарезервоване слово у JavaScript для оголошення класів:
class MyClass {
constructor() { }
}
JSX транспілюється у JavaScript, тому використання class як імені властивості призведе до синтаксичної помилки. DOM API використовує властивість element.className, тому React слідує цій конвенції.
Ні, не обов'язково, але настійно рекомендується для продакшн-проєктів. TypeScript:
- Виявляє помилки на етапі компіляції, а не під час виконання.
- Забезпечує автодоповнення та IntelliSense у редакторі.
- Покращує підтримуваність коду у великих командах.
Для навчання та прототипів можна використовувати JavaScript (.jsx), але для серйозних проєктів TypeScript (.tsx) є індустріальним стандартом.
React покаже попередження у консолі:
Warning: Each child in a list should have a unique "key" prop.
Наслідки:
- Повільний рендеринг: React не може ефективно визначити, які елементи змінилися.
- Неправильний стан: Якщо елементи мають внутрішній стан (input-поля, чекбокси), React може прив'язати стан до неправильного елемента після зміни порядку списку.
Правильно:
{items.map(item => <div key={item.id}>{item.name}</div>)}
Ні, if — це інструкція (statement), а не вираз. Усередині {} працюють лише вирази.
Альтернативи:
- Тернарний оператор:
{isLoggedIn ? <p>Привіт!</p> : <p>Увійдіть</p>}
- Логічний оператор
&&:
{isLoggedIn && <p>Привіт!</p>}
- IIFE (Immediately Invoked Function Expression):
{(() => {
if (role === 'admin') return <AdminPanel />;
if (role === 'user') return <UserPanel />;
return <GuestPanel />;
})()}
- Винесення логіки у змінну:
let content;
if (role === 'admin') content = <AdminPanel />;
else if (role === 'user') content = <UserPanel />;
else content = <GuestPanel />;
return <div>{content}</div>;
Що таке React
Філософія React — декларативний підхід до побудови UI, Virtual DOM, reconciliation та однонаправлений потік даних. Розуміння того, як React перетворює JSX на реальний DOM та чому це ефективніше за пряму маніпуляцію DOM.
Компоненти — будівельні блоки інтерфейсу
Фундаментальні принципи компонентної архітектури React — що таке компонент, як створювати функціональні компоненти, композиція проти успадкування, передача пропсів, деструктуризація, children та найкращі практики побудови переповторно використовуваних UI-блоків.