От JSX к обычным объектам: как работают элементы, компоненты и атрибуты React
Узнайте, во что преобразуется JSX при компиляции, почему компоненты — это просто функции, и как на основе этой концепции возникают практики использования пропсов, детей компонентов, значений по умолчанию, распространения свойств и слотов.
Источники: 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 может безопасно пропускать, повторять и перестраивать операции, а вы можете анализировать каждый компонент отдельно. В режиме строгой проверки компоненты даже отрендериваются дважды во время разработки, чтобы выявить нечистую логику.
Нарушение принципа чистоты приводит к появлению багов, проявляющихся далеко от их источника. Соблюдая его, ваши компоненты останутся предсказуемыми, их будет легко тестировать и обслуживать по мере роста приложения.