Главная / Статьи

От 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 может безопасно пропускать, повторять и перестраивать операции, а вы можете анализировать каждый компонент отдельно. В режиме строгой проверки компоненты даже отрендериваются дважды во время разработки, чтобы выявить нечистую логику.

    Нарушение принципа чистоты приводит к появлению багов, проявляющихся далеко от их источника. Соблюдая его, ваши компоненты останутся предсказуемыми, их будет легко тестировать и обслуживать по мере роста приложения.