From Html To Project

JSX та TSX — синтаксичний цукор, а не магія

Глибоке розуміння JSX та TSX — як саме JSX перетворюється на виклики функцій, що таке React.createElement та сучасний jsx-runtime, різниця між HTML та JSX, особливості типізації TSX у TypeScript та найпоширеніші помилки початківців.

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.

Що таке синтаксичний цукор?Синтаксичний цукор (syntactic sugar) — це синтаксис у мові програмування, який не додає нової функціональності, але робить код читабельнішим та зручнішим для людини. Під капотом він перетворюється на базові конструкції мови.Приклади синтаксичного цукру:
  • 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);
Ключовий момент:JSX — це лише зручна нотація для виклику 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: "Привіт!" });
}

Переваги нового трансформу:

  1. Менше бойлерплейту: Не потрібно імпортувати React у кожному файлі.
  2. Менший розмір бандлу: Якщо файл містить лише JSX без хуків або інших React API, імпорт React не додається.
  3. Швидша компіляція: Функції _jsx / _jsxs оптимізовані для швидкості.
Як налаштовується jsx-runtime?У 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>
React покаже попередження у консолі:
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 властивість
onclickonClick
onchangeonChange
onfocusonFocus
onbluronBlur
onsubmitonSubmit
tabindextabIndex
maxlengthmaxLength
readonlyreadOnly
autocompleteautoComplete
autofocusautoFocus
Виняток: data-* та aria-* атрибутиАтрибути з префіксами data- та aria- залишаються у kebab-case, як і в HTML:
<div 
  data-user-id="42"
  aria-label="Закрити модальне вікно"
>
  Контент
</div>
Це відповідає стандартам W3C для кастомних атрибутів та доступності.

Відмінність 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-colorbackgroundColor
font-sizefontSize
margin-topmarginTop
z-indexzIndex
border-radiusborderRadius

Числові значення (пікселі):

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/>
Prettier та ESLint автоматично форматують це за стандартом.

Відмінність 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 Умовний рендеринг через тернарний оператор
Файли проєкту
src
App.tsx
App.css
localhost:3000

Збирання 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>
Коли використовувати Fragment?
  • Для списків без обгортки: Коли потрібно повернути кілька елементів із .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:

  1. Перевірка типів пропсів: Якщо ви передасте label як число замість рядка, отримаєте помилку на етапі компіляції.
  2. Автодоповнення у редакторі: VS Code підказує доступні властивості props.
  3. Захист від 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
onClickMouseEvent<HTMLButtonElement>
onChange (input)ChangeEvent<HTMLInputElement>
onSubmit (form)FormEvent<HTMLFormElement>
onFocusFocusEvent<HTMLInputElement>
onKeyDownKeyboardEvent<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 Типізація компонента з пропсами
Файли проєкту
src
App.tsx
App.css
localhost:3000

Збирання 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 працює лише якщо:
  1. Список ніколи не змінюється (статичні дані).
  2. Елементи не можуть бути відсортовані або відфільтровані.
  3. Елементи не мають внутрішнього стану (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>
  );
}
Автоматичне вставлення крапки з комою (ASI)JavaScript автоматично вставляє ; після 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

АспектHTMLJSX
Атрибут класуclass="box"className="box"
Label forfor="id"htmlFor="id"
Inline-стиліstyle="color: red;"style={{ color: 'red' }}
Обробники подійonclick="..." (lowercase)onClick={...} (camelCase)
Самозакривні теги<img> або <img />Обов'язково <img />
Коментарі<!-- коментар -->{/* коментар */}
Вставка зміннихНеможлива напряму{variable}
Кілька кореневих тегівДопускаєтьсяПотрібна обгортка або <Fragment>
Логіка у розмітціНемає (лише статичний HTML)Вирази JavaScript через {}
Атрибути data-* / aria-*kebab-casekebab-case (виняток)

Інструменти для роботи з JSX/TSX

Babel REPL — онлайн-транспілятор

Щоб побачити, як JSX перетворюється на JavaScript, використовуйте Babel REPL:

  1. Відкрийте https://babeljs.io/repl
  2. Увімкніть пресет @babel/preset-react
  3. Введіть 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: "Параграф" })
  ]
})

Ключові відмінності:

  1. children завжди у props: Дочірні елементи передаються як props.children, а не окремими аргументами.
  2. Розділення jsx та jsxs:
    • jsx() — для елементів без дочірніх або з одним дочірнім.
    • jsxs() — для елементів з масивом дочірніх (множинні children).
  3. key як окремий параметр: Не входить у props, а передається третім аргументом.
  4. Автоматичний імпорт: Компілятор сам додає імпорт з react/jsx-runtime.
Чому jsx та jsxs окремо?Це оптимізація продуктивності. Коли React знає, що дочірній елемент один (jsx()), він уникає створення масиву children. Це зменшує навантаження на garbage collector та прискорює рендеринг.

Візуалізація різниці

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "JSX Код" as JSX #DBEAFE {
  note as JSXCode
    <div id="app">
      <h1>Заголовок</h1>
    </div>
  end note
}

rectangle "Класичний трансформ\n(React 16)" as Classic #FEF3C7 {
  note as ClassicCode
    React.createElement(
      "div",
      { id: "app" },
      React.createElement(
        "h1", 
        null, 
        "Заголовок"
      )
    )
    
    **Потребує:**
    import React from 'react';
  end note
}

rectangle "Сучасний трансформ\n(React 17+)" as Modern #DCFCE7 {
  note as ModernCode
    jsxs("div", {
      id: "app",
      children: jsx("h1", {
        children: "Заголовок"
      })
    })
    
    **Потребує:**
    import { jsx, jsxs } 
    from 'react/jsx-runtime';
    (автоматично)
  end note
}

JSX --> Classic : Babel\n@babel/preset-react\n{"runtime": "classic"}
JSX --> Modern : Babel\n@babel/preset-react\n{"runtime": "automatic"}

Classic --> "React Element\n(об'єкт)" as Element1 #E2E8F0
Modern --> "React Element\n(об'єкт)" as Element2 #E2E8F0

note bottom of Element1
  Однаковий результат —
  обидва повертають
  React Element
end note
@enduml

Спеціальні пропси: 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 Типізована форма реєстрації
Файли проєкту
src
App.tsx
App.css
localhost:3000

Збирання React застосунку…

Ключові моменти прикладу:

  1. Типізація стану: useState<FormData> та useState<FormErrors> забезпечують типобезпеку.
  2. Типізація подій: FormEvent<HTMLFormElement> та ChangeEvent<HTMLInputElement> для обробників.
  3. Валідація форми: Функція validateForm() повертає boolean та оновлює стан помилок.
  4. Умовний рендеринг: Після успішної відправки показується інший компонент.
  5. Використання 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.
Наступні кроки:У наступній лекції ми детально розглянемо React-компоненти — як створювати, композувати та повторно використовувати компоненти для побудови складних інтерфейсів.

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

Copyright © 2026