Модуль 1 · React: первый проект и первый бот

Справочник по Angular и React · модуль 1 · первый проект и первый бот

1.2

1.2

JSX
и компоненты

В этой главе разобрано, что такое JSX и по каким правилам он записывается, что такое компонент и как разнести компоненты по отдельным файлам. Заготовка доски заявок из главы 1.1 делится на три компонента: шапку, форму заявки и карточку заявки, и получает стили. В конце главы на доске три одинаковые карточки; как сделать их разными, разобрано в главе 1.3.

Категория
React
Уровень
Начальный
Что понадобится
Проект zayavki из главы 1.1 и запущенный npm run dev
Результат
Доска заявок из компонентов Header, RequestForm и RequestCard со стилями

§1Что такое JSX

Сравнение из жизни

Бланк заявления состоит из постоянного текста и пустых полей: «Прошу отремонтировать ____ в кабинете ____». Постоянный текст одинаков для всех, а в поля каждый раз вписывается своё.

Бланк заполняют один раз, а JSX пересчитывается каждый раз, когда меняются данные, и поля в нём заполняются автоматически.

JSX — запись разметки внутри кода JavaScript или TypeScript. Он выглядит как HTML: теги, атрибуты, вложенность. В отличие от HTML, в разметку JSX можно подставить любое значение из кода: переменную, вычисление, вызов функции. Место подстановки обозначается фигурными скобками, как пустое поле бланка.

Пример: подстановка значения в JSX
const room = 214
return <p>Заявка из кабинета {room}</p>

В первой строке объявлена переменная room со значением 214. Во второй строке JSX записан после слова return: функция возвращает абзац. Текст «Заявка из кабинета» — постоянная часть, а на место {room} при выводе подставится 214. На странице получится «Заявка из кабинета 214». Внутри фигурных скобок может стоять любое выражение JavaScript, например {room + 1} или {'каб. ' + room}.

Во что превращается JSX

Браузер JSX не выполняет. Когда браузер запрашивает файл компонента, Vite переводит каждый тег в вызов функции. Ниже — часть того, что Vite отдал браузеру вместо шапки, которая будет создана в §3. Этот код записывается не руками; он показан, чтобы было видно, откуда берутся правила JSX.

Что получает браузер вместо Header.tsx (сокращено)
_jsxDEV("header", {
  className: "header",
  children: [
    _jsxDEV("h1", { children: "Доска заявок" }),
    _jsxDEV("p", { children: "ИТ-отдел колледжа · заявки студентов и преподавателей" })
  ]
})

Каждый тег стал вызовом функции _jsxDEV. Первым передаётся имя тега ("header"), вторым — объект с атрибутами, а вложенные теги попадают в список children (дети). Из этого следуют два правила JSX, которые встретятся в главе. Первое: функция возвращает одно значение, поэтому у разметки, которую возвращает компонент, должен быть один общий внешний элемент. Второе: атрибуты становятся полями объекта JavaScript, поэтому их имена подчиняются правилам JavaScript — отсюда className вместо class (§3) и htmlFor вместо for (§4).

JSX описывает кусок интерфейса. Чтобы этот кусок можно было использовать много раз, его оборачивают в функцию — компонент.

§2Что такое компонент

Сравнение из жизни

В конструкторе одну деталь — например, окно — делают по одной форме и ставят в модель столько раз, сколько нужно. Если форму окна изменить, изменятся все окна в модели.

Детали конструктора одинаковы, а компоненту можно передать данные, и одна и та же форма даст разные окна — это предмет главы 1.3.

Компонент — функция, которая возвращает JSX. Имя компонента пишется с заглавной буквы: Header, RequestForm. Компонент используется в разметке другого компонента как тег: <Header />. По заглавной букве React отличает компонент от обычного тега HTML: <header> — элемент страницы, <Header /> — вызов функции Header и вывод того, что она вернула.

В главе 1.1 в проекте уже был один компонент — App в файле App.tsx. Держать всю страницу в одном компоненте неудобно: файл растёт, и в нём трудно найти нужное место. Поэтому интерфейс делят на компоненты по смыслу, и каждый компонент кладут в отдельный файл. Для доски заявок их будет три: Header — шапка с названием, RequestForm — форма новой заявки, RequestCard — карточка одной заявки. Компонент App собирает их на странице.

Как компонент попадает из одного файла в другой

Файл компонента заканчивается строкой export default Header. Слово export делает функцию доступной другим файлам, default — делает её главной частью файла. В файле, где компонент нужен, его подключают строкой import Header from './components/Header'. Путь начинается с ./ — «от папки текущего файла», расширение .tsx не пишется: Vite находит файл сам. Имя после import выбирается при подключении, но принято использовать то же, что у функции.

§3Первый компонент: шапка

Работа идёт в проекте zayavki, сервер разработки запущен командой npm run dev, страница localhost:5173 открыта в браузере (глава 1.1, «Как вернуться к проекту»). Компоненты хранятся в отдельной папке src/components, чтобы не смешивать их с файлами main.tsx и index.css.

  1. Создайте папку components

    В проводнике VS Code щёлкните по папке src, чтобы выделить её. Наведите курсор на строку REACT-KURS: справа появятся кнопки. Нажмите вторую — New Folder… (папка с плюсом). Под src появится поле ввода; введите components и нажмите Enter.

    Проводник VS Code: внутри src поле ввода с именем components
    Новая папка внутри src: имя components набрано в поле ввода
  2. Создайте файл Header.tsx

    Щёлкните по новой папке components, затем нажмите первую кнопку — New File… (лист с плюсом). Введите Header.tsx с заглавной буквы и нажмите Enter. Файл откроется справа пустым.

    Проводник VS Code: внутри components поле ввода с именем Header.tsx
    Новый файл внутри components: имя Header.tsx
  3. Вставьте код шапки и сохраните
    zayavki/src/components/Header.tsx
    function Header() {
      return (
        <header className="header">
          <h1>Доска заявок</h1>
          <p>ИТ-отдел колледжа · заявки студентов и преподавателей</p>
        </header>
      )
    }
    
    export default Header
    VS Code: файл components/Header.tsx с функцией Header
    Так должен выглядеть Header.tsx: 10 строк, файл в папке components

Код шапки разобран по строкам. function Header() { … } объявляет компонент — функцию с именем Header, которая ничего не получает (пустые скобки) и возвращает разметку. После return стоит JSX в круглых скобках: скобки нужны, чтобы разметку можно было начать с новой строки. Внешний элемент — <header>, тег HTML для шапки страницы или раздела. Внутри него заголовок <h1> и абзац <p> с подписью. Последняя строка export default Header делает компонент доступным другим файлам.

У тега <header> записан атрибут className="header". В HTML этот атрибут называется class, но слово class в JavaScript занято: им объявляют классы. Поэтому в JSX атрибут называется className, а в браузере он превращается в обычный class. По имени класса стили из index.css находят элемент; стили будут добавлены в §6.

Подключение шапки в App

Шапка создана, но на странице её нет: файл main.tsx выводит только App, а в App шапка пока не подключена. Замените содержимое src/App.tsx (выделить всё, удалить, вставить, сохранить):

zayavki/src/App.tsx
import Header from './components/Header'

function App() {
  return (
    <>
      <Header />
      <main>
        <p>Здесь появятся форма заявки и список карточек.</p>
      </main>
    </>
  )
}

export default App

Первая строка подключает компонент из файла components/Header.tsx. В разметке <Header /> выводит шапку; косая черта в конце означает, что у тега нет содержимого. Заголовок <h1> из App убран: он теперь в шапке.

В начале разметки стоят пустые угловые скобки <>, в конце — </>. Компонент App теперь возвращает два элемента подряд — шапку и <main>, а функция может вернуть только одно значение (§1). Пустые скобки — это фрагмент: обёртка, которая объединяет несколько элементов в один для React, но в браузере никакого тега не создаёт. Если обернуть в <div>, на странице появится лишний блок; фрагмент этого не делает.

Страница: заголовок Доска заявок, подпись ИТ-отдел колледжа, ниже строка Здесь появятся форма заявки и список карточек
Страница после сохранения: шапка выведена, стилей у неё пока нет

На странице появились заголовок и подпись из шапки, под ними — абзац из <main>. Шапка прижата к левому краю без отступов: правило main из index.css на неё не действует, потому что шапка стоит вне <main>.

Проверка: что будет без общей обёртки

Правило одного внешнего элемента проще запомнить, если один раз увидеть ошибку. Замените содержимое Header.tsx кодом ниже — в нём убран тег <header>, и функция возвращает два элемента подряд — и сохраните файл:

Header.tsx без общей обёртки (для опыта)
function Header() {
  return (
    <h1>Доска заявок</h1>
    <p>ИТ-отдел колледжа · заявки студентов и преподавателей</p>
  )
}

export default Header
Оверлей ошибки Vite: PARSE_ERROR Adjacent JSX elements must be wrapped in an enclosing tag, строка 4 файла Header.tsx, Help: Did you want a JSX fragment
Ошибка Vite поверх страницы: соседние элементы JSX без общей обёртки

Vite не смог перевести файл и показал ошибку поверх страницы. Строка [plugin:vite:oxc] Transform failed with 1 error означает, что перевод файла (transform) не удался. Дальше сама ошибка: Adjacent JSX elements must be wrapped in an enclosing tag — «соседние элементы JSX должны быть обёрнуты в общий тег». Ниже указаны файл и место: src/components/Header.tsx:4:5 — строка 4, символ 5, где начинается второй элемент. Последняя строка — подсказка: Did you want a JSX fragment <>...</>? — «может быть, нужен фрагмент». Та же ошибка видна в терминале, где работает Vite.

Верните правильный код шапки: вставьте в Header.tsx код из шага 3 и сохраните. Ошибка исчезнет сама, перезагружать страницу не нужно.

Шапка содержит только текст. В форме заявки появятся поля ввода, и с ними — ещё несколько правил JSX.

§4Форма заявки и правила атрибутов

Форма собирает заявку: что случилось, в каком кабинете, к какой категории относится и подробности. В этой главе форма только выводится на странице. Как сохранять введённый текст и отправлять заявку, разобрано в главах 1.4 и 1.6.

  1. Создайте файл RequestForm.tsx

    Так же, как в §3: выделите папку components, нажмите New File…, введите RequestForm.tsx.

  2. Вставьте код формы и сохраните
    zayavki/src/components/RequestForm.tsx
    function RequestForm() {
      return (
        <form className="form">
          <h2>Новая заявка</h2>
    
          <label htmlFor="title">Что случилось</label>
          <input id="title" type="text" placeholder="Например: не работает проектор" />
    
          <label htmlFor="room">Кабинет</label>
          <input id="room" type="text" placeholder="214" />
    
          <label htmlFor="category">Категория</label>
          <select id="category">
            <option>Компьютер</option>
            <option>Проектор</option>
            <option>Принтер</option>
            <option>Интернет</option>
          </select>
    
          <label htmlFor="details">Подробности</label>
          <textarea id="details" rows={3} />
    
          <button type="submit">Отправить заявку</button>
        </form>
      )
    }
    
    export default RequestForm

Форма состоит из пар «подпись — поле». Внешний элемент — <form className="form">, внутри заголовок <h2>, четыре пары и кнопка. В коде формы встречаются три правила JSX, которых не было в шапке.

Правило 1: htmlFor вместо for

Тег <label> — подпись к полю. Атрибут htmlFor="title" связывает подпись с полем, у которого id="title". Связь нужна по двум причинам: щелчок по подписи ставит курсор в поле, а программа чтения экрана для незрячих пользователей читает подпись, когда пользователь попадает в поле. В HTML этот атрибут называется for, но for в JavaScript — слово для цикла, поэтому в JSX пишут htmlFor. Это тот же случай, что с className.

Правило 2: каждый тег закрыт

В HTML у некоторых тегов нет закрывающей пары: <input>, <img>, <br>. В JSX закрытым должен быть каждый тег. Тег без содержимого закрывается косой чертой в конце: <input … />, <textarea … />. Если черту пропустить, Vite покажет ошибку Expected corresponding JSX closing tag for 'input' — «ожидался закрывающий тег для input».

Правило 3: числа и выражения — в фигурных скобках

У поля подробностей записано rows={3} — высота поля в три строки. Значение в кавычках JSX передаёт как строку, а в фигурных скобках — как значение JavaScript, здесь число 3. Для чисел, переменных и вычислений используются скобки, для постоянного текста — кавычки.


Остальные теги формы — обычный HTML. <input type="text"> — однострочное поле, placeholder — серая подсказка внутри пустого поля. <select> — выпадающий список, внутри него варианты <option>. <textarea> — многострочное поле. <button type="submit"> — кнопка отправки формы.

Кнопка пока перезагружает страницу

По умолчанию браузер при отправке формы перезагружает страницу. Поэтому, если нажать «Отправить заявку», страница мигнёт и поля очистятся. В главе 1.6 отправка будет перехвачена кодом, и перезагрузка прекратится.

§5Карточка заявки

Карточка показывает одну заявку: кабинет, статус, тему и подробности. Создайте в папке components файл RequestCard.tsx так же, как в §3, вставьте код и сохраните:

zayavki/src/components/RequestCard.tsx
function RequestCard() {
  return (
    <article className="card">
      <div className="card__top">
        <span className="card__room">каб. 214</span>
        <span className="card__status">Новая</span>
      </div>
      <h3>Не работает проектор</h3>
      <p>Проектор включается, но изображения на экране нет.</p>
    </article>
  )
}

export default RequestCard

Внешний элемент — <article>: тег HTML для самостоятельного блока, который имеет смысл и отдельно от страницы, как заметка или карточка. Верхняя строка card__top содержит кабинет и статус в двух элементах <span>. Ниже заголовок заявки <h3> и описание <p>. Заголовки идут по уровням: h1 — название страницы в шапке, h2 — разделы «Новая заявка» и «Заявки», h3 — заявка внутри раздела. Программы чтения экрана строят по уровням заголовков оглавление страницы.

Имена классов записаны по одному правилу: имя блока (card), два подчёркивания и имя части блока (room, status, top). Такое правило называется БЭМ (блок, элемент, модификатор). По имени card__status сразу видно, что это статус внутри карточки, и стили разных компонентов не пересекаются.

Текст карточки пока записан прямо в коде: каждая карточка будет показывать один и тот же проектор в кабинете 214. Это сделано намеренно, к этому вернётся §7.

§6Сборка страницы и стили

Все три компонента готовы. Компонент App собирает из них страницу: шапка сверху, ниже в две колонки форма и список заявок. Замените содержимое src/App.tsx и сохраните:

zayavki/src/App.tsx
import Header from './components/Header'
import RequestForm from './components/RequestForm'
import RequestCard from './components/RequestCard'

function App() {
  return (
    <>
      <Header />
      <main className="board">
        <RequestForm />
        <section className="list">
          <h2>Заявки</h2>
          <RequestCard />
          <RequestCard />
          <RequestCard />
        </section>
      </main>
    </>
  )
}

export default App

Три строки import подключают компоненты из папки components. Разметка обёрнута во фрагмент, как в §3. Внутри <main className="board"> два блока: форма <RequestForm /> и раздел <section className="list"> с заголовком и тремя карточками. Каждое <RequestCard /> — отдельный вызов функции RequestCard, поэтому на странице появятся три карточки.

Страница без стилей: поля формы в одну строку, три карточки текстом одна под другой
Страница после сохранения App.tsx: все компоненты на месте, стилей для них ещё нет

Все элементы на странице, но выглядят как обычный документ: поля формы стоят в одну строку, карточки не отделены друг от друга. Классы header, board, form, card уже есть в разметке, не хватает правил, которые к ним относятся. Замените содержимое src/index.css и сохраните:

zayavki/src/index.css
:root {
  font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
  color: #1f2430;
  background: #f6f7f9;
}

body {
  margin: 0;
}

.header {
  padding: 24px 20px;
  background: #1f2430;
  color: #ffffff;
  text-align: center;
}

.header h1 {
  margin: 0 0 4px;
}

.header p {
  margin: 0;
  color: #b8c0d4;
}

.board {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: 24px;
  max-width: 960px;
  margin: 0 auto;
  padding: 24px 20px;
}

.form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px;
  background: #ffffff;
  border: 1px solid #dde1ea;
  border-radius: 8px;
}

.form h2,
.list h2 {
  margin: 0 0 8px;
  font-size: 20px;
}

.form input,
.form select,
.form textarea {
  margin-bottom: 8px;
  padding: 8px;
  border: 1px solid #c4cad8;
  border-radius: 6px;
  font: inherit;
}

.form button {
  padding: 10px;
  border: 0;
  border-radius: 6px;
  background: #2f6fde;
  color: #ffffff;
  font: inherit;
  cursor: pointer;
}

.list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.card {
  padding: 14px 16px;
  background: #ffffff;
  border: 1px solid #dde1ea;
  border-radius: 8px;
}

.card__top {
  display: flex;
  justify-content: space-between;
  font-size: 14px;
}

.card__room {
  color: #5b6478;
}

.card__status {
  padding: 2px 8px;
  border-radius: 10px;
  background: #e3ecff;
  color: #2f6fde;
}

.card h3 {
  margin: 8px 0 4px;
  font-size: 18px;
}

.card p {
  margin: 0;
  color: #3d4557;
}

@media (max-width: 700px) {
  .board {
    grid-template-columns: 1fr;
  }
}
Готовая доска: тёмная шапка, слева форма Новая заявка, справа раздел Заявки с тремя одинаковыми карточками
Так должна выглядеть страница: шапка, форма слева, три карточки справа

Стили привязаны к классам из компонентов. Правило .header даёт шапке тёмный фон, белый текст и выравнивание по центру. Правило .board делит страницу на две колонки. display: grid включает сетку. grid-template-columns: 320px 1fr задаёт левую колонку шириной 320 пикселей, а правой колонке отдаёт всё оставшееся место: 1fr означает одну долю свободного места. Правила .form и .list ставят свои элементы в столбик (display: flex и flex-direction: column) с промежутком gap. Правила .card… оформляют карточку: рамку, скругление углов, кабинет слева и статус справа в верхней строке. Последнее правило @media (max-width: 700px) на узком экране, например телефона, ставит форму и список в одну колонку.

VS Code: в проводнике папка components с файлами Header.tsx, RequestCard.tsx, RequestForm.tsx; открыт App.tsx
Так должен выглядеть проект: три компонента в папке components, App.tsx их подключает

В проводнике в папке src/components три файла компонентов, в редакторе открыт App.tsx с тремя строками import. Если у вас так же, а страница выглядит как на кадре выше, проект готов к главе 1.3.

§7Дерево компонентов

Компоненты вложены друг в друга, и вместе они образуют дерево — так же, как в главе 0.1. Корень дерева — App, он содержит шапку, форму и три карточки.

Дерево компонентов доски заявок

App Header RequestForm Card Card Card три вызова RequestCard — три одинаковые карточки: текст записан внутри компонента

Голубым отмечены три вызова одного компонента RequestCard. Функция одна и та же, и данных снаружи она не получает, поэтому все три карточки выглядят одинаково.

На доске три карточки с одним и тем же текстом: «Не работает проектор, каб. 214». Чтобы показать три разные заявки, можно было бы сделать три компонента — ProjectorCard, PrinterCard, InternetCard, — но тогда на сотню заявок понадобится сотня компонентов с одинаковой разметкой. Компонент нужно сделать один, а данные — тему, кабинет, статус — передавать ему при вызове, как значения в поля бланка. Механизм передачи данных в компонент называется props и разобран в главе 1.3.

§8Частые ошибки

Все сообщения ниже получены в проекте zayavki на Vite 8 и React 19. Ошибки разбора JSX Vite показывает поверх страницы и в терминале; ошибки React видны в консоли браузера (F12 → вкладка Console); ошибки TypeScript VS Code подчёркивает красным в редакторе.

Что видноПочемуЧто сделать
Поверх страницы: Adjacent JSX elements must be wrapped in an enclosing tagКомпонент возвращает несколько элементов подряд без общей обёрткиОбернуть их в общий тег или во фрагмент <>…</>
Поверх страницы: Expected corresponding JSX closing tag for 'input'Тег без содержимого не закрытПоставить косую черту: <input … />
Поверх страницы: HTML comments are not allowed in modulesВ JSX записан комментарий HTML <!-- … -->Комментарий в JSX пишется так: {/* текст */}
В консоли: Invalid DOM property `class`. Did you mean `className`?Атрибут записан как в HTMLЗаменить class на className
В консоли: Invalid DOM property `for`. Did you mean `htmlFor`?Атрибут подписи записан как в HTMLЗаменить for на htmlFor
Страница пустая, в консоли: Header is not defined; в редакторе: Cannot find name 'Header'. Did you mean 'Headers'?Компонент используется, но не подключён строкой import. Headers в подсказке — другой, встроенный объект браузера, он здесь не нуженДобавить в начало файла import Header from './components/Header'
Страница пустая, в консоли: The requested module '/src/components/Header.tsx' does not provide an export named 'default'В файле компонента нет строки export default HeaderДобавить её в конец файла компонента
Вместо шапки на странице пустой блок, ошибок нетКомпонент назван с маленькой буквы (function header), и <header /> React выводит как обычный тег HTMLНазвать компонент с заглавной буквы и использовать как <Header />

§9Проверьте себя

Что выведет на страницу код const count = 3 и <p>Заявок: {count + 1}</p>?

«Заявок: 4». В фигурных скобках стоит выражение JavaScript, и на его место подставляется результат вычисления.

Почему в JSX пишут className и htmlFor, а не class и for?

Атрибуты JSX становятся полями объекта JavaScript, а class и for в JavaScript — занятые слова: первым объявляют классы, вторым — циклы. Поэтому React использует имена className и htmlFor и в браузере превращает их в обычные class и for.

Чем фрагмент <>…</> отличается от обёртки <div>…</div>?

Оба объединяют несколько элементов в один, как требует JSX. <div> появляется на странице как отдельный блок и может повлиять на стили и раскладку. Фрагмент в браузере никакого элемента не создаёт.

Как React отличает компонент Header от тега header?

По первой букве. Имя с маленькой буквы React считает тегом HTML и создаёт такой элемент на странице. Имя с заглавной буквы — компонент: React вызывает функцию с этим именем и выводит то, что она вернула.

Что нужно сделать, чтобы компонент из файла RequestCard.tsx можно было использовать в App.tsx?

В конце файла RequestCard.tsx написать export default RequestCard, а в начале App.tsx — import RequestCard from './components/RequestCard'. После этого в разметке App можно писать <RequestCard />.

Почему на доске три одинаковые карточки и почему не стоит делать для каждой заявки свой компонент?

Компонент RequestCard вызывается три раза, а его текст записан внутри функции, поэтому результат одинаковый. Отдельный компонент на каждую заявку означает повтор одной и той же разметки столько раз, сколько заявок. Правильно сделать один компонент и передавать ему данные — это props из главы 1.3.

Шпаргалка

Компонентfunction Header() { return (…) }export default Header
Подключениеimport Header from './components/Header'<Header />
Значение в JSX<p>Кабинет {room}</p>
Несколько элементов<> … </>
АтрибутыclassName="card"htmlFor="title"rows={3}
Комментарий в JSX{/* текст */}

Итог главы

JSX — разметка внутри кода, в которую в фигурных скобках подставляются значения. Vite переводит каждый тег в вызов функции. Поэтому у разметки компонента один внешний элемент или фрагмент <>…</>, каждый тег закрыт, а атрибуты называются по правилам JavaScript: className, htmlFor, числа в фигурных скобках. Компонент — функция с именем с заглавной буквы, которая возвращает JSX; он хранится в своём файле, отдаётся строкой export default и подключается строкой import. Доска заявок собрана из компонентов Header, RequestForm и RequestCard и оформлена стилями. Три карточки на ней одинаковые, потому что компонент не получает данных снаружи.

Источники

  1. react.dev — Writing Markup with JSX
  2. react.dev — JavaScript in JSX with Curly Braces
  3. react.dev — Your First Component
  4. react.dev — Importing and Exporting Components
  5. MDN — элемент label
Справочник по Angular и React © Макаров Максим Николаевич