Головна / Статті

Від JSX до звичайних об’єктів: як працюють елементи, компоненти та параметри React

Дізнайтеся, до чого компілюється JSX, чому компоненти — це просто функції, та як з цієї єдиної концепції випливають пропси, дочірні елементи, значення за замовчуванням, розповсюдження та слоти.

Видавець: REACTAPP.TOP9 хв читання1983 слів

Джерела: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Багато помилок у React виникають через нечітке розуміння того, що насправді таке JSX, компоненти та пропси. Як тільки ви дізнаєтесь, що JSX — це виклик функції, що цей виклик створює звичайний об’єкт, а компонент — це функція, яка отримує один об’єкт аргументів, правила, які здаються довільними, починають мати сенс. У цій статті будується така модель, починаючи з результатів компіляції, а потім вона застосовується до пропсів, children, значень за замовчуванням, механізму розповсюдження значень, принципів незмінності та шаблонів композиції, які ви будете використовувати щодня.

JSX — це виклики функцій, а не HTML

JSX часто називають „HTML усередині JavaScript“, що є неповним описом. Точніше кажучи, JSX — це синтаксичний „цукор“, який компілятор перетворює на звичайні виклики функцій.

Розгляньмо простий заголовок:

const element = <h1 className="title">Hello World</h1>;

Компілятор на кшталт Babel перетворює це на виклик React.createElement, передаючи назву тега, об’єкт з атрибутами та вміст:

const element = React.createElement("h1", { className: "title" }, "Hello World");

Це класична трансформація. З версії React 17 більшість інструментальних комплексів використовують автоматичний рунтайм JSX, який замість цього виконує виклики функції jsx(), імпортованої з react/jsx-runtime, тож React більше не потрібен у межах цього процесу. Ідея залишається такою ж: маркап перетворюється на виклик функції.

Цей виклик повертає звичайний об’єкт JavaScript, який називається елементом React:

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

Елемент React — це не що інше, як опис того, що має з’явитися на екрані. React пройшовся по дереву цих об’єктів та визначив, як привести справжній DOM у відповідність з ним; саме це дерево описів і є тим, що люди зазвичай мають на увазі під Віртуальним DOM.

Що пояснює модель

Дотримання принципу «JSX — це виклик функції, який повертає об’єкт», розв’язує кілька поширених проблем:

  • Відсутність операторів if всередині JSX. Неможливо розміщувати оператори всередині аргумента функції, тому використовуються тернарні вирази або оператор &&, або логіка переміщується перед оператором return.
  • className замість class. JSX — це JavaScript, і там class є зарезервованим словом.
  • Один кореневий елемент. Функція повертає одне значення, тому компонент мусить повертати один елемент (який може містити багато дочірніх елементів).

Компонент — це функція, яка повертає елементи

Компоненти — це функції, значення яких є елементом React або null, щоб нічого не відображати.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

Не існує жодної прихованої механіки, і в сучасному React немає потреби у класах. React викликає функцію щоразу, коли йому потрібно знати, що відобразити. Коли дані змінюються, React знову її викликає, отримує нову деревоподібну структуру елементів, порівнює її з попередньою та оновлює лише ті частини DOM, які відрізняються. Це порівняння називається примиренням.

Два правила, яких дотримується кожен компонент

Імена мають починатися з великої літери

JSX використовує правила написання для визначення значення тегів. Тег, написаний з великої літери, посилається на компонент, тож React викликає вашу функцію:

// ✅ React treats this as a component
<Greeting />

Тег, написаний малою літерою, розглядається як вбудований елемент DOM, тому React намагатиметься створити елемент <greeting> безпосередньо, замість того щоб викликати якусь функцію:

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Повертайте рівно один кореневий елемент

Неможливо повернути два сусідні елементи поруч, оскільки це еквівалентно поверненню двох значень з однієї функції:

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

Існує два способи вирішення. Обгорніть ці елементи у батьківський елемент або використайте Fragment, записаний у форматі <>...</>, який об’єднує їх без додавання додаткового вузла до DOM. У наведеному нижче фрагменті показані обидві версії поспіль.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Віддавайте перевагу Fragment, якщо тільки обгортка не потрібна для стилізації чи макетування.

Props — це аргументи функції

Якщо компоненти є функціями, то props — це їхні аргументи. Тут компонент отримує значення name з об’єкта, який йому надісланий:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

І тут він відображається з цим налаштованим атрибутом:

// Usage
<Greeting name="Devendra" />

React збирає всі атрибути, вказані у тегу JSX, у один об’єкт props та передає його як перший аргумент. У цьому полягає весь механізм.

Розбирання об’єкта у сигнатурі

Повторення props. швидко робить код галасливим. Розбирання об’єкта у списку параметрів дозволяє назвати кожен використовуваний атрибут та одразу показати вхідні дані компонента:

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Місце виклику залишається незмінним:

<Greeting name="Devendra" role="Senior Engineer" />

Значення за замовчуванням із розбиранням

Якщо атрибут є необов’язковим, вкажіть йому значення за замовчуванням безпосередньо у шаблоні розбирання. Якщо role не передано, використовується значення "Developer":

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Такий спосіб використання відображає значення ролі за замовчуванням:

// role will be "Developer" if not provided
<Greeting name="Devendra" />

Розбирання значень за замовчуванням діє лише тоді, коли значення є undefined. Явне передавання null обхідно для цього механізму, що варто пам’ятати, коли параметри надходять з даних API.

children: композиція, вбудована у синтаксис

Контент, розташований між вхідними та закривальними тегами компонента, автоматично стає параметром children. Компонент Card може просто відображати все, що йому надано всередині його обгортки:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

Той, хто викликає компонент, вирішує, що буде всередині:

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

Композиція через children — це спосіб створення гнучких та повторно використовуваних компонентів. Замість того, щоб передбачати кожну можливу варіацію за допомогою окремого параметра, компонент забезпечує структуру, а батьківський компонент — контент. Бібліотеки на кшталт shadcn/ui та Material UI створюють діалоги, картки та кнопки саме таким чином.

Будь-яке значення JavaScript може бути пропом

Круглі дужки в JSX означають «обчислити це як JavaScript», тому проп може містити будь-яке значення: рядки, числа, логічні значення, масиви, об’єкти чи функції. Цей компонент отримує кілька типів значень одночасно, умовно відображає індикатор, перетворює масив на елементи списку та під’єднує функцію-відповідь до кнопки:

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

Місце виклику показує, як передаються кожні типи значень:

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Важливі моменти:

  • Звичайні рядкові значення можна вказати безпосередньо у лапках.
  • Усе інше, включаючи числа, логічні значення, масиви, об’єкти та функції, потрібно поміщати в круглі дужки.
  • Параметр у вигляді функції, такий як onDelete, — це спосіб, яким дитинний елемент повідомляє про події своєму батьківському елементу. Саме ця схема калебеків дозволяє ефективно керувати станом: стан знаходиться у спільному батьківському елементі, а дитинні елементи змінюють його лише шляхом виклику функцій, які передає батьківський елемент.
  • Параметри є тільки для читання

    Ніколи не змінюйте параметри. Присвоєння значень полям об’єкта параметра тихо змінює дані, якими володіє батьківський елемент:

    // ❌ Never do this
    function Greeting({ user }) {
      user.name = "Modified"; // This will cause silent bugs
      return <h1>{user.name}</h1>;
    }
    

    Краще отримати нове значення та залишити оригінальне недоторканим:

    // ✅ Create a new value instead
    function Greeting({ user }) {
      const displayName = user.name.toUpperCase();
      return <h1>{displayName}</h1>;
    }
    

    Дані передаються лише в одному напрямку — від батьківського до дитинного елемента, і саме це робить додатки на React прогнозованими. Дитинний елемент, якому потрібно, щоб батьківський елемент щось змінив, викликає функцію, надану батьком.

    Передавання параметрів за допомогою rest та spread

    Коли компонент оточує елемент DOM, часто хочеться приймати багато атрибутів, не вказуючи кожен з них окремо. Синтаксис rest у параметрі виділяє ті властивості, якими сам компонент керує (тут label), а решту збирає у inputProps, який потім передається елементу input:

    function Input({ label, ...inputProps }) {
      return (
        <div>
          <label>{label}</label>
          <input {...inputProps} />
        </div>
      );
    }
    

    Кожен додатковий атрибут, такий як type, placeholder чи onChange, передається без змін до базового елемента:

    // All extra props (type, placeholder, onChange, etc.) flow straight to <input>
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      onChange={handleChange}
    />
    

    Бібліотеки постійно використовують цю схему, але розміщення довільних властивостей на елементі DOM може призвести до передачі значень, які він не розуміє, що спричиняє попередження про невідомі атрибути. Ретельно обмірковуйте, що та де ви розміщуєте. У прикладі також відображається label, який не пов’язаний із вхідним елементом; у реальному коді їх потрібно з’єднати за допомогою htmlFor та id, щоб був доступним.

    Три рівні гнучкості компонентів

    Компоненти зазвичай розвиваються через певні етапи у міру зростання вимог.

    Рівень 1: по одній властивості на кожну варіацію

    Найпростіший кнопка приймає окрему властивість для кожного зображення, яке може відображати:

    function Button({ text, color, icon }) {
      return <button style={{ color }}>{icon} {text}</button>;
    }
    

    Кожна нова варіація потребує ще одного параметра, і API росте без меж. Цю проблему часто називають вибухом параметрів; вона відрізняється від процесу передачі даних крізь багато шарів, що називається «бурінням параметрів».

    Рівень 2: складання з дочірніми елементами

    Дозволяючи користувачам передавати вміст через children, можна усунути більшість цих параметрів. Кнопка зберігає лише variant для форматування:

    function Button({ children, variant }) {
      return <button className={`btn btn-${variant}`}>{children}</button>;
    }
    

    Тепер користувач контролює те, що відображається всередині, включаючи іконки:

    <Button variant="primary">
      <Icon name="save" /> Save Changes
    </Button>
    

    Рівень 3: іменовані слоти

    Коли компоненту потрібні кілька окремих областей, можна приймати JSX через іменовані параметри. Макет сторінки може включати заголовок, бічну панель та основний вміст:

    function PageLayout({ header, sidebar, children }) {
      return (
        <div className="layout">
          <header>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    

    Кожна область заповнюється у місці виклику:

    <PageLayout
      header={<Navbar />}
      sidebar={<FilterPanel />}
    >
      <ProductList />
    </PageLayout>
    

    Слоти — це звичайні параметри, які містять JSX: ті, хто викликає компонент, контролюють те, що заповнює кожну область, компонент контролює структуру, а лейаут залишається ефективним навіть при зміні вимог. Щоб детальніше дізнатися про цей шлях рефакторингу, перегляньте як усунути перевантаження параметрами в React за допомогою композиції та слотів.

    Коротко про модель

    • JSX: синтаксичний «цукор» для викликів функцій створення елементів, таких як React.createElement().
    • Елемент React: звичайний об’єкт JavaScript, який описує інтерфейс користувача.
    • Компонент: функція, яка повертає елементи React.
    • Параметри: єдиний об’єкт аргументів, який передається цій функції.
    • children: спеціальний параметр, який містить вкладений контент JSX.
  • Зміна пропсів: ніколи не дозволяється; сприймайте пропси як тільки для читання.
  • Чистота — це правило, яке дозволяє масштабуватися

    Усе це залежить від чистоти: однакові пропси завжди мають призводити до однакового результату. Коли процес відображення не має побічних ефектів, не змінює пропси та не модифікує зовнішній стан, React може безпечно пропускати, повторювати та переупорядковувати операції, а ви можете аналізувати кожен компонент окремо. У режимі Strict Mode навіть компоненти відображаються двічі під час розробки, щоб виявити нечисту логіку.

    Якщо порушити принцип чистоти, ви створюєте умови для появи багів, які проявляються далеко від їхньої причини. Дотримуйтесь його, і ваші компоненти залишатимуться прогнозованими, легкими для тестування та підтримки у міру зростання додатку.