MakeFirstStep· Frontend + ИИ

Урок 22 · React Lite (бонус)

React: зачем он нужен. Vite, JSX и первый компонент

В уроке 11 вы написали render(state), который заново рисует экран по данным. Это правильная идея, но кода вокруг неё много: найти элемент, создать, заполнить, вставить, не забыть обновить. React берёт эту работу на себя: вы описываете, что должно быть на экране, а как добраться до этого — решает он. Сегодня вы увидите, почему так делают большие сайты, создадите проект на Vite и соберёте страницу из компонентов.

  • Назвать, чем ручной DOM мешает расти приложению, и сравнить один и тот же интерфейс на чистом JS и на React
  • Разбить страницу на компоненты и дать им правильные имена
  • Создать проект командой npm create vite@latest -- --template react и прочитать его структуру
  • Писать JSX и знать, чем он отличается от HTML: className, htmlFor, закрытые теги, один корень, {…}, style={{…}}
  • Объявлять компоненты, экспортировать их и импортировать в App.jsx
  • Попросить ИИ переписать знакомый JS-блок на React и разобрать ответ построчно
Как работать с этой страницей

Примеры на React открываются в песочнице с превью: вкладка App.jsx с кодом, под ней — готовая страница и консоль. Меняйте код и нажимайте ▶ Запустить (или Ctrl + Enter). React подгружается из интернета, поэтому нужен доступ в сеть. В песочнице один файл — компоненты лежат рядом друг с другом; в настоящем проекте на Vite каждый будет в отдельном файле. Строка setTimeout(() => console.log(…), 100) внизу примеров — служебная автопроверка: она печатает в консоль HTML, который построил React, чтобы задачу можно было проверить. В задачах её не удаляйте; в обычном проекте она не нужна. Блоки с заголовком «Терминал» и именем файла — это команды и файлы для вашего компьютера, у них нет кнопки запуска.

01 Зачем React: где ручной DOM начинает мешать

Всё, что вы делали с экраном до сих пор, — это императивный подход: вы пошагово командуете браузеру. «Найди этот элемент. Создай новый. Запиши в него текст. Вставь сюда». Для карточки с заголовком это выглядит так:

Карточка курса: чистый JS · Ctrl + Enter
<div id="app"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Тот же результат на React. Сравните: здесь нет ни одного createElement, ни одного querySelector. Вы пишете то, что хотите увидеть, — функция просто «возвращает» разметку.

Карточка курса: React · Ctrl + Enter
export default function App() {
  return (
    <div className="card">
      <h2>JavaScript с нуля</h2>
      <p>24 урока · 8 недель</p>
      <button>Записаться</button>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

В консоли обеих песочниц — одна и та же разметка. Разница в том, как вы к ней пришли: в первом случае 12 строк команд, во втором — «картинка» того, что нужно.

Аналогия из жизни

Чойхона. Ручной DOM — это когда вы сами идёте на кухню и говорите повару: «Возьми казан. Налей масло. Положи лук. Через пять минут добавь морковь…» На каждый плов — вся инструкция заново. React — это когда вы говорите официанту: «Плов и чай на двоих». Что именно и в каком порядке делать на кухне — забота заведения. Вы описываете результат (так это называется — декларативный подход), а не шаги.

Четыре боли ручного DOM

1. Разметку не видно
Структура страницы спрятана за десятками createElement и append. Чтобы понять, что получится, нужно мысленно «выполнить» код.
2. Легко забыть обновить
Одни и те же данные показаны в нескольких местах. Поменяли в одном — в другом осталось старое, и экран врёт.
3. Нет «своих тегов»
Карточка курса нужна в десяти местах — приходится писать функции-фабрики и склеивать вручную. Нет единого способа сказать «вот мой блок, используй его как обычный тег».
4. Состояние и экран расходятся
В уроке 11 вы решили это функцией render(), которая перестраивает экран целиком. Работает, но при каждом изменении браузер пересоздаёт всё — и теряются фокус в поле ввода, прокрутка и выделение.

Посмотрите на вторую боль вживую. Цена курса показана в двух местах, а обновлена только в одном:

Экран врёт: забыли обновить второе место · Ctrl + Enter
document.body.innerHTML = `
  <p>Курс: <b id="price-top">500</b> сомони</p>
  <p>Итого: <b id="price-total">500</b> сомони</p>`;

let price = 500;
price = 450; // сделали скидку

document.querySelector("#price-top").textContent = price;
// про #price-total забыли

console.log(document.querySelector("#price-top").textContent);
console.log(document.querySelector("#price-total").textContent);

Вверху 450, в «Итого» всё ещё 500. В маленькой странице это видно сразу, в большом приложении с сотней экранов — нет. React устроен так, что экран всегда считается из данных, и «забыть» обновить место просто нечем.

Что такое React

React — библиотека для построения интерфейсов, её создала компания Meta (бывший Facebook), и сейчас она, пожалуй, самая популярная в мире для сайтов и приложений. Ключевая идея: интерфейс — это функция от данных. Данные поменялись — React сам сравнит «было» и «стало» и изменит в DOM только то, что нужно. Всё остальное (маршруты, серверный рендеринг) подключается отдельно: на следующем уровне курса вы встретите Next.js — это фреймворк на основе React.

React нужен не всегда

Лендинг из пяти блоков и пары кнопок прекрасно живёт на чистом HTML, CSS и JS — и загружается быстрее. React окупается там, где много интерактивных экранов, одни и те же блоки повторяются, а данные часто меняются: личные кабинеты, магазины, дашборды, чаты. Мы учим React не вместо JS, а поверх него: всё, что вы знаете про функции, массивы и объекты, пригодится на каждой строке.

Практика: закрепляем

Сначала прочитаем ручной DOM, потом починим экран, который «врёт», и наконец переведём код на JSX.

ЗАДАЧА 1.1

Предскажите: что построил код

лёгкая

Сначала предскажите три строки вывода, потом запустите. Обратите внимание, как много кода нужно ради одного списка из трёх пунктов.

Можно редактировать · Ctrl + Enter
const list = document.createElement("ul");

for (const dish of ["Оши палав", "Самбуса", "Курутоб"]) {
  const li = document.createElement("li");
  li.textContent = dish;
  list.append(li);
}

console.log(list.children.length);
console.log(list.firstElementChild.textContent);
console.log(list.outerHTML);
Ожидаемый результат
3
Оши палав
<ul><li>Оши палав</li><li>Самбуса</li><li>Курутоб</li></ul>
Подсказка 1

list.children — это дочерние элементы списка. Цикл создаёт по одному li на каждое блюдо.

Подсказка 2

outerHTML показывает элемент вместе с его собственными тегами — строкой, без пробелов между li.

ЗАДАЧА 1.2

Починить «врущий» экран

средняя

Цена показана в трёх местах: «Курс», «Итого» и «Взнос в месяц» (цена, делённая на 4). Допишите render() так, чтобы он обновлял все три места из state.price. Нижние строки менять не нужно: они меняют цену, вызывают render() и печатают результат. Это ровно та работа, которую в React вы не пишете.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <p>Курс: <b id="price-top">500</b> сомони</p>
  <p>Итого: <b id="price-total">500</b> сомони</p>
  <p>Взнос в месяц: <b id="price-month">125</b> сомони</p>`;

const state = { price: 500 };

function render() {
  // TODO: обновите все три места по state.price
}

state.price = 480;
render();

console.log(document.querySelector("#price-top").textContent);
console.log(document.querySelector("#price-total").textContent);
console.log(document.querySelector("#price-month").textContent);
Ожидаемый результат
480
480
120
Подсказка 1

Для каждого места: document.querySelector("#id").textContent = …. Три элемента — три строки внутри render.

Подсказка 2

Взнос — это state.price / 4. Число запишется в textContent как строка само.

ЗАДАЧА 1.3

Из createElement в JSX

средняя

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

JavaScript
const plan = document.createElement("section");
plan.className = "plan";

const h = document.createElement("h3");
h.textContent = "Тариф «Старт»";

const ul = document.createElement("ul");
const li1 = document.createElement("li");
li1.textContent = "24 урока";
const li2 = document.createElement("li");
li2.textContent = "Проект в портфолио";
ul.append(li1, li2);

plan.append(h, ul);
Тариф «Старт» · Ctrl + Enter
export default function App() {
  return (
    // TODO: верните разметку тарифа
    <div></div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<section class="plan">
<h3>Тариф «Старт»</h3>
<ul>
<li>24 урока</li>
<li>Проект в портфолио</li>
</ul>
</section>
Подсказка 1

Корневой элемент — <section className="plan">, внутри него h3 и ul с двумя li. Вложенность в JSX — как в HTML.

Подсказка 2

Замените заглушку <div></div> целиком, вместе с комментарием: комментарий внутри return (…) перед тегом можно просто удалить.

Читать подробнее: react.dev — Quick Start · learn.javascript.ru — DOM-дерево

02 Мыслим компонентами

Откройте любой сайт и мысленно обведите рамками «кирпичики»: шапка, меню, карточка товара, кнопка, подвал. Это и есть компоненты. Компонент — функция, которая возвращает кусок интерфейса. Раз это функция, вы уже умеете с ней обращаться: объявить один раз, вызвать сколько нужно.

Аналогия из жизни

Конструктор или набор формочек для самбусы. Одну форму вы сделали один раз — и лепите ею хоть сто штук. Страница сайта — это не одна большая лепёшка, а набор готовых деталей: шапка, список курсов, подвал. Нужно поменять подвал — меняете в одном месте, и он обновится на всех страницах.

Страница — это дерево

Компоненты вкладываются друг в друга, как вложенные коробки. Самая внешняя — App, главный компонент приложения. Внутри неё лежат остальные:

App
Header
Logo
Nav
Courses
Footer
Одна страница — одно дерево компонентов. В коде оно читается сверху вниз так же, как вложенный HTML
App ├─ Header │ ├─ Logo │ └─ Nav ├─ Courses └─ Footer

Как найти границы компонентов

  • Блок повторяется (карточка курса, пункт меню) — почти всегда это отдельный компонент.
  • У блока своя задача («показать шапку», «показать подвал») — отдельный компонент, как отдельная функция в уроке 5.
  • Файл разросся, и в нём не найти нужное — режем на части.
  • Не дробите до пылинок. Тег <h2> компонентом делать незачем. Хорошее правило: если для блока можно придумать понятное имя, оно получится и у компонента.

Первый компонент

Компонент выглядит так же, как функция из урока 5, только он возвращает не число, а разметку. Два правила: имя начинается с большой буквы и использовать его нужно как тег:

Три компонента на одной странице · Ctrl + Enter
function Header() {
  return (
    <header className="top">
      <h1>MakeFirstStep</h1>
    </header>
  );
}

function Footer() {
  return <footer>© MakeFirstStep, Душанбе</footer>;
}

export default function App() {
  return (
    <div className="page">
      <Header />
      <main>Здесь будут курсы</main>
      <Footer />
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Обратите внимание на три вещи. <Header /> с большой буквы — это наш компонент: React вызывает функцию Header и подставляет результат. <main> с маленькой — обычный тег HTML. App тоже компонент, просто самый главный. Его мы пишем как export default function App(): именно его песочница (и проект Vite) показывает на странице.

Частая ошибка: имя с маленькой буквы

React различает теги и компоненты по первой букве. <header /> — это тег HTML, а <Header /> — ваша функция. Если назвали компонент header или menu, React не найдёт функцию и нарисует пустой тег (а <menu> — вообще настоящий тег HTML). Ошибки не будет — просто пустота на экране, и это самое неприятное.

Практика: закрепляем

ЗАДАЧА 2.1

Это имя компонента?

лёгкая

Напишите функцию isComponentName(name): она возвращает true, если имя начинается с заглавной латинской буквы, и false — если нет.

Можно редактировать · Ctrl + Enter
function isComponentName(name) {
  // TODO: вернуть true или false
  return false;
}

for (const name of ["Header", "footer", "CourseCard", "nav", "App"]) {
  console.log(name + ": " + isComponentName(name));
}
Ожидаемый результат
Header: true
footer: false
CourseCard: true
nav: false
App: true
Подсказка 1

Первая буква строки: name[0]. Нужно проверить, что она заглавная.

Подсказка 2

Сравните букву с её версией в верхнем регистре: name[0] === name[0].toUpperCase(). Либо регулярное выражение /^[A-Z]/.test(name).

ЗАДАЧА 2.2

Допишите подвал

средняя

В App уже стоит <Footer />, но самого компонента нет — страница падает с ошибкой. Объявите Footer: он возвращает тег <footer> с текстом © MakeFirstStep, Душанбе.

Подвал страницы · Ctrl + Enter
function Header() {
  return (
    <header>
      <h1>MakeFirstStep</h1>
    </header>
  );
}

// TODO: объявите компонент Footer

export default function App() {
  return (
    <div>
      <Header />
      <main>
        <p>Курсы скоро</p>
      </main>
      <Footer />
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div>
<header>
<h1>MakeFirstStep</h1>
</header>
<main>
<p>Курсы скоро</p>
</main>
<footer>© MakeFirstStep, Душанбе</footer>
</div>
Подсказка 1

Образец — Header выше. Функция называется Footer (с большой буквы) и возвращает один тег.

Подсказка 2

function Footer() { return <footer>© MakeFirstStep, Душанбе</footer>; }

ЗАДАЧА 2.3

Найдите баг: пустое меню

средняя

Меню должно показывать два блюда, но на странице пусто, а ошибок в консоли нет. Найдите причину и исправьте.

Пустое меню · Ctrl + Enter
function menu() {
  return (
    <ul>
      <li>Оши палав</li>
      <li>Самбуса</li>
    </ul>
  );
}

export default function App() {
  return (
    <div>
      <h2>Меню</h2>
      <menu />
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div>
<h2>Меню</h2>
<ul>
<li>Оши палав</li>
<li>Самбуса</li>
</ul>
</div>
Подсказка 1

Запустите и посмотрите на консоль: вместо <ul> там пустой тег <menu></menu>. React принял <menu /> за обычный HTML-тег.

Подсказка 2

Имя компонента должно начинаться с большой буквы — и в объявлении функции, и в месте использования.

Читать подробнее: react.dev — Your First Component · react.dev — Thinking in React

03 Создаём проект: Vite + React

В уроке 17 вы создавали проект на Vite с шаблоном vanilla. Для React меняется один параметр — шаблон. Всё остальное вам знакомо: npm, package.json, dev-сервер.

Аналогия из жизни

Шаблон проекта — как типовая планировка квартиры. Можно самому класть кирпичи и тянуть проводку (настраивать сборщик вручную), а можно получить дом «под ключ» с готовой проводкой и водой и сразу заселяться — добавлять свою мебель. Vite даёт именно такой «дом под ключ» для React.

Пошаговая инструкция

  1. Откройте терминал в папке для проектов (без кириллицы и пробелов в пути; не в OneDrive/iCloud). Node.js уже установлен с урока 17 — проверьте: node -v.
  2. Создайте проект. Часть -- --template react говорит Vite: «возьми шаблон React». Двойное -- передаёт опцию дальше, в create-vite (мы уже видели это на уроке 17).
  3. Перейдите в папку и установите зависимости — npm install скачает React и Vite в node_modules.
  4. Запустите сервер разработки и откройте адрес из терминала.
Терминал
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

Если вы запустите npm create vite@latest без опций, мастер задаст вопросы: название проекта, фреймворк (выберите React), вариант (JavaScript). В зависимости от версии он может ещё предложить сразу установить зависимости и запустить сервер — можно согласиться, а можно ответить «нет» и выполнить команды вручную, как выше. После npm run dev вы увидите:

Терминал — вывод
&gt; my-react-app@0.0.0 dev
&gt; vite

  VITE v8.3.4  ready in 210 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Откройте http://localhost:5173/: вы увидите стартовую страницу React со счётчиком. Измените что-нибудь в src/App.jsx и сохраните — страница обновится сама, без перезагрузки (HMR из урока 17).

Если что-то пошло не так

«npm: command not found» — Node.js не установлен или терминал нужно перезапустить. Ошибка про версию Node — обновите Node.js до LTS. Страница пустая — откройте DevTools (F12) → Console: там написано, в каком файле ошибка (чаще всего опечатка в JSX). Порт занят — Vite сам выберет 5174 и напишет об этом.

Что создал Vite

my-react-app/ ├─ index.html единственная HTML-страница; внутри — пустой <div id="root"> ├─ package.json react и react-dom в dependencies; скрипты dev / build / preview ├─ vite.config.js подключает плагин React: он умеет читать JSX ├─ .gitignore уже содержит node_modules и dist ├─ public/ файлы «как есть» (иконка сайта) └─ src/ ├─ main.jsx точка входа: «посадить» React на страницу ├─ App.jsx главный компонент — сюда пишем свой код ├─ App.css стили App ├─ index.css общие стили страницы └─ assets/ картинки, которые импортируются из JS

Точный набор файлов зависит от версии шаблона (например, там могут быть настройки линтера), но три файла всегда одни и те же: index.html, src/main.jsx и src/App.jsx. Рассмотрим их по цепочке.

Цепочка запуска: index.html → main.jsx → App.jsx

index.html (фрагмент)
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

Страница пустая: только контейнер #root и подключённый модуль main.jsx. Всё остальное нарисует React.

src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
  1. import … from 'react-dom/client' — подключаем часть React, которая умеет работать с браузерным DOM. Имя без ./ — это пакет из node_modules (урок 17).
  2. import App from './App.jsx' — берём главный компонент из соседнего файла. Он экспортирован через export default, поэтому в импорте нет фигурных скобок.
  3. document.getElementById('root') — наш старый знакомый: находим контейнер из index.html. Это последний раз, когда вы вручную ищете элемент в DOM.
  4. createRoot(…).render(…) — «рисуй компонент <App /> внутри этого контейнера». Дальше за всем DOM следит React.
  5. StrictMode — режим подсказок для разработки: он нарочно вызывает компоненты дважды, чтобы быстрее выявить ошибки. В собранном сайте (npm run build) этого нет.
Почему console.log в компоненте печатается дважды

Если в проекте на Vite вы напишете console.log("render Header") внутри компонента, то увидите две одинаковые строки. Это не баг: так работает StrictMode в режиме разработки. В песочнице курса его нет, поэтому здесь каждая строка печатается один раз. Подробнее об этом поговорим на уроке 24.

src/App.jsx (то, что вы увидите в шаблоне — сокращённо)
import { useState } from 'react'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>Get started</h1>
      <button onClick={() => setCount((count) => count + 1)}>
        Count is {count}
      </button>
    </>
  )
}

export default App

В шаблоне уже есть счётчик с useState — это тема урока 23, сегодня он нам не нужен. Первое, что делают с новым проектом, — чистят шаблон.

Чистим шаблон

  1. Удалите картинки из src/assets/ (и импорты на них в App.jsx).
  2. Очистите src/App.css (оставьте пустым) — свои стили напишем позже.
  3. Замените содержимое src/App.jsx на минимальный компонент:
src/App.jsx — чистый старт
function App() {
  return (
    <div>
      <h1>Привет, MakeFirstStep!</h1>
    </div>
  )
}

export default App

Сохраните: в браузере появится ваш заголовок. Мы будем писать function App() и отдельной строкой export default App — шаблон Vite делает именно так, а песочница на странице допускает короткий вариант export default function App(). Оба варианта делают одно и то же.

Практика: закрепляем

ЗАДАЧА 3.1

Прочитайте package.json

лёгкая

Ниже — package.json нового проекта, записанный строкой. Разберите его через JSON.parse и выведите три строки: имя проекта, команду скрипта dev и список зависимостей (dependencies) через запятую.

Можно редактировать · Ctrl + Enter
const raw = `{
  "name": "my-react-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" },
  "devDependencies": { "@vitejs/plugin-react": "^6.1.1", "vite": "^8.3.0" }
}`;

const pkg = JSON.parse(raw);

// TODO: имя проекта
// TODO: команда dev
// TODO: имена зависимостей через запятую
Ожидаемый результат
my-react-app
vite
react, react-dom
Подсказка 1

Имя — pkg.name, команда — pkg.scripts.dev. Каждое выведите через console.log.

Подсказка 2

Имена зависимостей — это ключи объекта: Object.keys(pkg.dependencies).join(", ").

ЗАДАЧА 3.2

Порядок команд

средняя

Преподаватель продиктовал команды вперемешку. Отсортируйте массив typed в правильном порядке запуска проекта, ориентируясь на массив order: команда стоит на месте того начала из order, с которого она начинается. Выведите команды по одной в строке.

Можно редактировать · Ctrl + Enter
const order = ["npm create", "cd", "npm install", "npm run dev"];

const typed = [
  "npm run dev",
  "cd my-react-app",
  "npm create vite@latest my-react-app -- --template react",
  "npm install",
];

// TODO: отсортируйте typed по порядку из order и выведите
Ожидаемый результат
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
Подсказка 1

Нужна функция, которая по команде находит её позицию: order.findIndex((start) => cmd.startsWith(start)). Сортируем по этой позиции.

Подсказка 2

Осторожно: "npm install" и "npm run dev" оба начинаются с "npm ", но в order стоят более длинные начала — они различаются. Используйте typed.sort((a, b) => pos(a) - pos(b)), где pos — ваша функция позиции.

Читать подробнее: vite.dev — Scaffolding Your First Vite Project · react.dev — Build a React app from Scratch

04 JSX: похож на HTML, но не HTML

JSX — это способ писать разметку прямо внутри JavaScript. Браузер JSX не понимает: Vite (а в нашей песочнице — встроенный компилятор) превращает каждый тег в вызов функции. Для вас это значит одно: JSX — это JavaScript, а значит, правила у него строже, чем у HTML.

Что вы пишете и во что это превращается (упрощённо)
// вы пишете:
const title = <h1 className="big">Салом</h1>;

// сборщик превращает в обычный JS:
const title = jsx("h1", { className: "big", children: "Салом" });
Аналогия из жизни

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

Шесть отличий JSX от HTML

HTMLJSXПочему
class="card"className="card"class — зарезервированное слово JS
for="name"htmlFor="name"for — это цикл в JS
<br>, <img …>, <input …><br />, <img … />, <input … />В JSX каждый тег обязан быть закрыт
Несколько элементов подрядОдин корневой элемент или <>…</>Функция возвращает одно значение
Текст и числа «как есть»{выражение} в фигурных скобкахТак JS-значения попадают в разметку
style="color: red"style={{ color: "red" }}Стиль — это объект, свойства в camelCase

1–3. className, htmlFor и закрытые теги

Форма на JSX · Ctrl + Enter
export default function App() {
  return (
    <form className="signup">
      <label htmlFor="name">Ваше имя</label>
      <input id="name" type="text" placeholder="Мадина" />
      <br />
      <button type="button">Записаться</button>
    </form>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

В консоли видно, что в итоговой разметке всё стало обычным HTML: class, for. Измените className на class и запустите: в React 19 страница ещё работает, но это считается ошибкой стиля — в правильном JSX пишем className.

Частая ошибка: незакрытый тег

В HTML <input> и <br> закрывать не нужно. В JSX — нужно, иначе компилятор остановится с ошибкой.

Это ошибка: Unexpected token / Unterminated JSX contents
<div>
  <input type="text">     {/* ✖ тег не закрыт */}
  <br>                    {/* ✖ и этот тоже */}
</div>

<div>
  <input type="text" />   {/* ✓ */}
  <br />                  {/* ✓ */}
</div>

4. Один корневой элемент

Компонент — это функция, а функция возвращает одно значение. Поэтому два тега рядом вернуть нельзя: их нужно обернуть. Если лишний <div> не нужен, используют фрагмент — пустые теги <>…</>. Он группирует детей, но в итоговом HTML ничего не добавляет.

Две версии: с div и с фрагментом
// ✖ ошибка: "Adjacent JSX elements must be wrapped in an enclosing tag"
return (
  <h2>Расписание</h2>
  <p>Пн, Ср, Пт — 18:00</p>
);

// ✓ с обёрткой
return (
  <div>
    <h2>Расписание</h2>
    <p>Пн, Ср, Пт — 18:00</p>
  </div>
);

// ✓ с фрагментом: лишнего div в HTML не будет
return (
  <>
    <h2>Расписание</h2>
    <p>Пн, Ср, Пт — 18:00</p>
  </>
);

Круглые скобки после return нужны не React, а JavaScript: если после return перейти на новую строку без скобок, JS решит, что возвращать нечего (return;), и вернёт undefined. Скобки — страховка для многострочной разметки.

5. Фигурные скобки: JavaScript внутри разметки

Всё, что внутри { }, — обычное JS-выражение: переменная, арифметика, вызов функции, тернарный оператор ? :. Результат подставляется в разметку. Скобки можно ставить и в тексте, и в значении атрибута.

Выражения в {…} · Ctrl + Enter
export default function App() {
  const student = "Мадина";
  const lessons = 24;
  const hour = 19;

  return (
    <div className="box" title={"Студент " + student}>
      <h2>Салом, {student}!</h2>
      <p>Осталось уроков: {lessons - 22}</p>
      <p>Имя в верхнем регистре: {student.toUpperCase()}</p>
      <p>{hour < 20 ? "Занятия ещё идут" : "Занятия закончились"}</p>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Можно в {…}
Переменные, числа, строки, арифметика, вызовы функций, ? :, &&, ||, методы строк и массивов.
Нельзя в {…}
Инструкции: if, for, const x = …. Это не выражения. Условие внутри разметки пишем через ? : или &&; а if и переменные — выше, перед return.
Комментарий в JSX
{/* вот так */} — это пустое выражение с комментарием внутри. Обычный // … внутри разметки превратится в текст на странице.

Что React рисует, а что нет

Значения true, false, null и undefined React не показывает вообще — на этом построено условное отображение. Но число 0 показывается, и об этом часто забывают.

Что исчезает, а что нет · Ctrl + Enter
export default function App() {
  const isOpen = true;
  const messages = 0;

  return (
    <div>
      <p>true: [{true}]</p>
      <p>null: [{null}]</p>
      <p>0: [{0}]</p>
      <p>{isOpen && "Мы открыты"}</p>
      <p>Новых сообщений: {messages && <b>есть</b>}</p>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

В последней строке вы ожидали пустоту, а получили 0: выражение 0 && … равно 0, а ноль React рисует. Безопасный вариант — сделать условие настоящим булевым: messages > 0 && … или тернарный оператор.

6. style — это объект

В HTML стиль — строка. В JSX style принимает объект, значит, фигурные скобки получаются двойными: внешние — «здесь JS», внутренние — сам объект. Свойства пишутся в camelCase (backgroundColor), числа без единиц считаются пикселями.

style как объект · Ctrl + Enter
export default function App() {
  const badge = {
    backgroundColor: "gold",
    padding: 8,
    borderRadius: 8,
    fontWeight: "bold",
  };

  return (
    <div>
      <p style={badge}>Новинка</p>
      <p style={{ color: "crimson", fontSize: 20 }}>Скидка 10%</p>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Для основного оформления всё равно используйте CSS-классы (className + style.css): style пригодится, когда значение вычисляется из данных (ширина полосы прогресса, цвет по условию).

ИИ-подсказка

Скопированный из интернета HTML — главный источник ошибок JSX. Если вставили готовую вёрстку и страница не открывается, попросите ИИ: «Переведи этот HTML в JSX: замени class и for, закрой одиночные теги, стили-строки переведи в объекты, оберни в один корень. Перечисли, что изменил». Список изменений сверьте с таблицей выше — так вы сразу запоминаете правила.

Практика: закрепляем

ЗАДАЧА 4.1

Переведите HTML в JSX

лёгкая

Ниже — готовая HTML-вёрстка. Переведите её в JSX внутри <div className="form">: замените class и for, закройте одиночные теги.

JavaScript
<label for="name">Ваше имя</label>
<input id="name" type="text" placeholder="Мадина">
<br>
<p class="hint">Мы перезвоним</p>
HTML → JSX · Ctrl + Enter
export default function App() {
  return (
    <div className="form">
      {/* TODO: вставьте сюда вёрстку в виде JSX */}
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div class="form">
<label for="name">Ваше имя</label>
<input id="name" placeholder="Мадина" type="text">
<br>
<p class="hint">Мы перезвоним</p>
</div>
Подсказка 1

Четыре замены: for → htmlFor, class → className, <input …> → <input … />, <br> → <br />.

Подсказка 2

Заглушку-комментарий {/* … */} замените четырьмя строками разметки. Порядок атрибутов оставьте как в HTML.

ЗАДАЧА 4.2

Подставьте значения

лёгкая

Вместо знаков ??? подставьте выражения в фигурных скобках: имя студента, цену курса и цену одного урока (цена, делённая на число уроков). Числа писать вручную нельзя — только переменные и арифметика.

Выражения в JSX · Ctrl + Enter
export default function App() {
  const student = "Мадина";
  const price = 480;
  const lessons = 24;

  return (
    <div>
      <h2>Добро пожаловать, ???!</h2>
      <p>Курс стоит ??? сомони, это ??? сомони за урок.</p>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div>
<h2>Добро пожаловать, Мадина!</h2>
<p>Курс стоит 480 сомони, это 20 сомони за урок.</p>
</div>
Подсказка 1

Каждое ??? заменяется на {student}, {price} и т. д. Текст вокруг остаётся как есть.

Подсказка 2

Цена за урок — выражение прямо в скобках: {price / lessons}.

ЗАДАЧА 4.3

Найдите баг: style-строка

средняя

Карточка акции выводится с ошибкой. Исправьте так, чтобы стиль применялся: переведите его в объект (свойства в camelCase, число 12 — пиксели).

Акция недели · Ctrl + Enter
export default function App() {
  return (
    <div style="background-color: gold; padding: 12px; border-radius: 8px;">
      Акция недели
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div style="background-color: gold; padding: 12px; border-radius: 8px;">Акция недели</div>
Подсказка 1

В консоли будет «Minified React error #62» — это как раз про style: React ждёт объект, а получил строку. Нужны двойные фигурные скобки: style={{ … }}.

Подсказка 2

style={{ backgroundColor: "gold", padding: 12, borderRadius: 8 }} — порядок свойств оставьте таким же, как в строке.

ЗАДАЧА 4.4

Найдите баг: два корня

средняя

Компонент возвращает два элемента подряд, и страница не запускается. Исправьте без лишнего div — в итоговом HTML должны остаться только h2 и p.

Расписание · Ctrl + Enter
export default function App() {
  return (
    <h2>Расписание</h2>
    <p>Пн, Ср, Пт — 18:00</p>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<h2>Расписание</h2>
<p>Пн, Ср, Пт — 18:00</p>
Подсказка 1

Нужна обёртка, которая не попадает в HTML. Как называется такая «невидимая» обёртка?

Подсказка 2

Фрагмент: откройте <> перед h2 и закройте </> после p.

ЗАДАЧА 4.5

Предскажите: что React нарисует

лёгкая

Предскажите, что React покажет в абзаце: впишите в prediction весь текст абзаца так, как он появится на странице (пустые скобки [] означают «ничего не нарисовано»). Затем запустите — код сам сравнит ваш ответ с настоящим.

Что нарисует React · Ctrl + Enter
const prediction = "";   // например: "[1] [] [x] …" — по одному значению в каждой паре скобок

export default function App() {
  return (
    <p>
      [{5 + 3}] [{false}] [{null}] [{0}] [{"a" + "b"}] [{true ? "да" : "нет"}] [{undefined}] [{10 > 5}]
    </p>
  );
}

// автопроверка: сравнивает ваш прогноз с тем, что построил React
setTimeout(() => console.log(document.getElementById("root").textContent.trim() === prediction.trim() ? "Верно!" : "Не угадали, подумайте ещё"), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Верно!
Подсказка 1

Не рисуются четыре значения: true, false, null, undefined. Число 0 рисуется.

Подсказка 2

Выражение 10 > 5 равно true, а true React не показывает.

ЗАДАЧА 4.6

Ловушка нуля

сложная

В корзине пусто, но вместо ничего на странице появился ноль. Исправьте так, чтобы при пустой корзине выводилось <p>Корзина пуста</p>, а при непустой — <p>Товаров: N</p>. Затем замените cart на ["Самбуса", "Чой"] и убедитесь, что показывается «Товаров: 2» (после проверки верните пустой массив).

Корзина · Ctrl + Enter
const cart = [];

export default function App() {
  return (
    <div>
      <h3>Корзина</h3>
      {cart.length && <p>Товаров: {cart.length}</p>}
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div>
<h3>Корзина</h3>
<p>Корзина пуста</p>
</div>
Подсказка 1

0 && … даёт 0, а ноль React рисует. Нужен тернарный оператор: условие ? А : Б — он выбирает одну из двух веток.

Подсказка 2

{cart.length > 0 ? <p>Товаров: {cart.length}</p> : <p>Корзина пуста</p>}

Читать подробнее: react.dev — Writing Markup with JSX · react.dev — JavaScript in JSX with Curly Braces · react.dev — Conditional Rendering

05 App.jsx: экспорт и импорт компонентов

Пока компоненты лежали в одном файле. В реальном проекте так не делают: один компонент — один файл. Это те же модули из урока 17: файл экспортирует функцию, другой файл её импортирует. Для компонентов принято export default.

Структура проекта с компонентами

src/ ├─ main.jsx ├─ App.jsx собирает страницу из компонентов └─ components/ ├─ Header.jsx export default function Header() ├─ CourseCard.jsx export default function CourseCard() └─ Footer.jsx export default function Footer()
  • Имя файла = имя компонента, с большой буквы: Header.jsx, CourseCard.jsx.
  • Расширение .jsx — признак того, что внутри JSX. Vite требует его для файлов с разметкой.
  • Папка components — просто договорённость, чтобы не сваливать всё в src/.

Как это выглядит в файлах

src/components/Header.jsx
export default function Header() {
  return (
    <header className="top">
      <h1>MakeFirstStep</h1>
    </header>
  )
}
src/components/Footer.jsx
export default function Footer() {
  return <footer>© MakeFirstStep, Душанбе</footer>
}
src/App.jsx
import Header from './components/Header.jsx'
import Footer from './components/Footer.jsx'

export default function App() {
  return (
    <div className="page">
      <Header />
      <main>Здесь будут курсы</main>
      <Footer />
    </div>
  )
}

Строка import Header from './components/Header.jsx' читается так: «возьми то, что Header.jsx экспортирует по умолчанию, и назови Header». Путь начинается с ./ (рядом с этим файлом), расширение .jsx пишем целиком — как мы договорились на уроке 17. Не было бы export default — импортировать было бы нечего.

Аналогия из жизни

Кухня большого ресторана. Салаты делает холодный цех, горячее — горячий цех, десерты — кондитерский. Каждый цех работает в своём помещении (файле) и отдаёт готовое блюдо (экспорт). Официант-сборщик (App.jsx) ничего не готовит: он берёт блюда из цехов (импорт) и составляет из них заказ на столе. Перепутать цеха нельзя, а новый повар приходит в конкретный цех, а не на всю кухню сразу.

Именованный экспорт тоже работает

Два способа — выберите один и держитесь его
// 1) по умолчанию — один компонент на файл (наш выбор)
export default function Header() { … }
import Header from './components/Header.jsx'

// 2) именованный — можно экспортировать несколько штук из файла
export function Header() { … }
import { Header } from './components/Header.jsx'

Главное — чтобы экспорт и импорт совпали по стилю: у export default импорт без скобок, у именованного — со скобками. В этом курсе компоненты экспортируем по умолчанию.

Три типичные ошибки импорта

1) Забыли export default — ошибка «does not provide an export named 'default'». 2) Импортировали без ./: import Header from 'components/Header.jsx' — Vite ищет такой пакет в node_modules и не находит. 3) Назвали компонент в импорте с маленькой буквы: import header from … — получится тег, а не ваш компонент (см. раздел 02).

Один компонент — много раз

Главный выигрыш компонентов — повторное использование: объявили один раз, расставили сколько нужно. Изменили компонент — поменялись все копии.

Один компонент, три копии · Ctrl + Enter
function CourseCard() {
  return (
    <article className="card">
      <h3>JavaScript</h3>
      <p>24 урока</p>
    </article>
  );
}

export default function App() {
  return (
    <main className="grid">
      <CourseCard />
      <CourseCard />
      <CourseCard />
    </main>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Попробуйте изменить текст внутри CourseCard — изменятся все три карточки. Сразу возникает вопрос: а если нужны разные названия — «HTML», «JavaScript», «React»? Для этого у компонентов есть props, мы разберём их на следующем уроке.

Заглянем на урок вперёд

Два самых важных слова урока 23: props — параметры компонента (как параметры функции из урока 5) и useState — память компонента, которая заставляет React заново рисовать экран при изменении данных. Нажмите на кнопку и посмотрите — код пока просто читайте, объяснять его мы будем на следующем занятии.

Тизер урока 23: счётчик · Ctrl + Enter
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Нажато раз: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Сравните с уроком 11: там были state, render(), querySelector и addEventListener. Здесь — один компонент, и ни одной строки про поиск элементов. Данные лежат в count, экран описан в return, а связку «изменили данные → перерисовали» React обеспечивает сам.

Практика: закрепляем

ЗАДАЧА 5.1

Строки импорта

лёгкая

Напишите функцию importLine(name), которая по имени компонента возвращает строку импорта для проекта на Vite: файл лежит в ./components/, имеет то же имя и расширение .jsx, в конце — точка с запятой.

Можно редактировать · Ctrl + Enter
const components = ["Header", "CourseCard", "Footer"];

function importLine(name) {
  // TODO: вернуть строку вида import Имя from "./components/Имя.jsx";
  return "";
}

for (const name of components) {
  console.log(importLine(name));
}
Ожидаемый результат
import Header from "./components/Header.jsx";
import CourseCard from "./components/CourseCard.jsx";
import Footer from "./components/Footer.jsx";
Подсказка 1

Имя встречается в строке дважды: после import и в пути. Удобно собрать строку шаблонной строкой в обратных кавычках.

Подсказка 2

return `import ${name} from "./components/${name}.jsx";`;

ЗАДАЧА 5.2

Уберите копипасту

средняя

Три карточки в App набраны копипастой, а компонент CourseCard объявлен, но не используется. Замените три копии на три использования <CourseCard />. Проверка видит, что компонент действительно вызвался три раза: в консоли должны появиться три строки «CourseCard готов».

Карточки курсов · Ctrl + Enter
function CourseCard() {
  console.log("CourseCard готов");
  return (
    <article className="card">
      <h3>JavaScript</h3>
      <p>24 урока</p>
    </article>
  );
}

export default function App() {
  return (
    <main>
      <article className="card">
        <h3>JavaScript</h3>
        <p>24 урока</p>
      </article>
      <article className="card">
        <h3>JavaScript</h3>
        <p>24 урока</p>
      </article>
      <article className="card">
        <h3>JavaScript</h3>
        <p>24 урока</p>
      </article>
    </main>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
CourseCard готов
CourseCard готов
CourseCard готов
<main>
<article class="card">
<h3>JavaScript</h3>
<p>24 урока</p>
</article>
<article class="card">
<h3>JavaScript</h3>
<p>24 урока</p>
</article>
<article class="card">
<h3>JavaScript</h3>
<p>24 урока</p>
</article>
</main>
Подсказка 1

Внутри <main> должны остаться только три тега-компонента. Внутри них ничего писать не нужно: содержимое уже внутри функции.

Подсказка 2

<CourseCard /> — самозакрывающийся тег с большой буквы. Напишите его три раза подряд.

ЗАДАЧА 5.3

Четыре компонента, один вложен в другой

сложная

Соберите страницу из четырёх компонентов: Logo (тег <span className="logo">MakeFirstStep</span>), Navbar (тег <nav>, внутри компонент Logo и ссылка <a href="#courses">Курсы</a>), Hero (<section className="hero"> с заголовком h1 «Frontend + ИИ») и Footer (<footer>© 2026 MakeFirstStep</footer>). Корнем App сделайте фрагмент <>…</>: Navbar, Hero, Footer по порядку.

Лендинг из компонентов · Ctrl + Enter
// TODO: объявите Logo, Navbar, Hero и Footer

export default function App() {
  return (
    <div>
      <p>Соберите страницу из компонентов</p>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<nav>
<span class="logo">MakeFirstStep</span>
<a href="#courses">Курсы</a>
</nav>
<section class="hero">
<h1>Frontend + ИИ</h1>
</section>
<footer>© 2026 MakeFirstStep</footer>
Подсказка 1

Сначала объявите Logo — он нужен Navbar. В JS порядок объявления функций не критичен, но читать удобнее от простого к сложному.

Подсказка 2

Navbar возвращает <nav><Logo /><a href="#courses">Курсы</a></nav>, а App — <><Navbar /><Hero /><Footer /></>.

Читать подробнее: react.dev — Importing and Exporting Components · learn.javascript.ru — Экспорт и импорт

06 ИИ-блок: переписываем знакомый JS на React

Лучший способ понять новый подход — взять код, который вы уже понимаете, и сравнить его с версией на React. ИИ здесь — переводчик, а вы — проверяющий. Вы знаете оригинал, значит, сразу видите, где перевод неточен.

Правила курса

1) Сначала 15 минут сами: попробуйте переписать блок на JSX без подсказок — в песочнице или в проекте на Vite. 2) Только потом промпт. 3) Не вставляйте в проект код, который не можете объяснить строчку за строчкой. 4) Результат проверяет запуск, а не уверенный тон ИИ.

Исходный блок на чистом JS

Это блок, похожий на те, что вы писали на уроках 8 и 11: карточка студента строится вручную.

JavaScript
const app = document.querySelector("#app");
const student = { name: "Мадина", group: "JS-12", score: 87 };

const box = document.createElement("div");
box.className = "profile";

const h = document.createElement("h2");
h.textContent = student.name;

const p = document.createElement("p");
p.textContent = `Группа ${student.group}, баллы: ${student.score}`;

box.append(h, p);
app.append(box);

Плохой и хороший промпт

Переделай в React

Почему плохо: ИИ не знает, где вы учитесь, какие инструменты у вас есть, и выдаст «всё и сразу» — с состоянием, эффектами и сторонними библиотеками, которых мы ещё не проходили. И вы не получите объяснений.

Контекст: я новичок, второй месяц учу JavaScript, сегодня впервые увидел React. Проект на Vite, шаблон React, JavaScript (не TypeScript), без дополнительных библиотек. Задача: вот код на чистом JS: [вставьте блок выше]. Перепиши его как один React-компонент App. Ограничения: только разметка и данные. Без useState, useEffect и обработчиков событий. Без document.querySelector и createElement. Никаких новых пакетов. Формат ответа: 1) код компонента; 2) таблица «строка в JS — что заменило её в React» — по строке на каждую строку исходного кода; 3) три отличия JSX от HTML, которые встретились в этом примере. Проверка: скажи, какие ошибки новички чаще всего делают при таком переводе.

Это развёрнутый вариант шаблона из шпаргалки курса: «Вот код на чистом JS: [код]. Перепиши как React-компонент и объясни построчно, что заменило querySelector и addEventListener». Мы добавили контекст, ограничения и формат — и убрали события, которые появятся на уроке 23.

Что должен вернуть ИИ

Ответ ИИ (типичный)
export default function App() {
  const student = { name: "Мадина", group: "JS-12", score: 87 };

  return (
    <div className="profile">
      <h2>{student.name}</h2>
      <p>Группа {student.group}, баллы: {student.score}</p>
    </div>
  );
}

Разбор построчно: что заменило что

Чистый JSReactЧто изменилось
document.querySelector("#app")— (нет)Контейнер #root находит main.jsx, один раз за весь проект
const student = { … }та же строкаДанные — обычный JS, ничего не меняется
createElement("div") + box.className = "profile"<div className="profile">Вместо двух команд — один тег; class стал className
createElement("h2") + h.textContent = student.name<h2>{student.name}</h2>Вместо textContent — фигурные скобки с выражением
Шаблонная строка `Группа ${…}`Группа {student.group}, …Текст и значения смешиваются прямо в разметке
box.append(h, p), app.append(box)вложенность теговКто в ком лежит — видно по отступам, не по вызовам
(нет)function App() { return … } и export defaultБлок стал компонентом: его можно вставить как <App /> и использовать повторно

Чек-лист: как проверять ответ ИИ

  • Имена компонентов с большой буквы и в объявлении, и в использовании.
  • className, htmlFor, закрытые теги, один корень — таблица из раздела 04.
  • style — объект, а не строка.
  • Лишнее. Если ИИ добавил useState, useEffect, axios или styled-components, хотя вы просили без них, — спросите «зачем это здесь?» и попросите убрать.
  • Запуск. Вставьте в песочницу или проект и откройте консоль: ошибок нет, результат совпадает с оригиналом.

Практика: закрепляем

ЗАДАЧА 6.1

Проверьте «ответ ИИ»: четыре ошибки

сложная

ИИ перевёл карточку студента на React, но допустил четыре ошибки из чек-листа. Найдите их все и исправьте. Страница должна показать карточку студента и под ней абзац «Данные студента». Запускайте после каждой правки и читайте консоль.

Ответ ИИ с ошибками · Ctrl + Enter
function profile() {
  const student = { name: "Мадина", group: "JS-12", score: 87 };
  return (
    <div class="profile" style="border: 1px solid gray">
      <h2>{student.name}</h2>
      <p>Группа {student.group}, баллы: {student.score}</p>
    </div>
  );
}

export default function App() {
  return (
    <profile />
    <p>Данные студента</p>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
<div class="profile" style="border: 1px solid gray;">
<h2>Мадина</h2>
<p>Группа JS-12, баллы: 87</p>
</div>
<p>Данные студента</p>
Подсказка 1

Ошибки по порядку: имя компонента с маленькой буквы; class вместо className; style строкой; два элемента в return без общей обёртки.

Подсказка 2

function Profile и <Profile />; className="profile"; style={{ border: "1px solid gray" }}; в App оберните два элемента в <>…</>.

ЗАДАЧА 6.2

Реальный запрос к ИИ

средняя

Возьмите свой блок из урока 8 или 11 (карточка, пункт списка, заголовок страницы) — только без обработчиков событий. Сначала 15 минут переписывайте его на JSX сами. Потом отправьте ИИ промпт по образцу выше с вашим кодом, получите ответ и составьте таблицу «строка в JS — что в React — что изменилось» (как в разборе выше). Положите результат в песочницу, запустите, убедитесь, что экран совпал с оригиналом. Эта задача проверяется преподавателем — отметьте её сами, когда закончите.

Подсказка 1

Если ответ ИИ не запускается, не просите «исправь» вслепую: скопируйте текст ошибки из консоли и спросите, что она означает и в какой строке причина.

Подсказка 2

Таблицу удобно вести в файле NOTES.md — она пригодится в домашнем задании.

Читать подробнее: react.dev — Writing Markup with JSX

07 Практика на уроке: разбиваем страницу на компоненты

Берём готовую страницу «MakeFirstStep», которая целиком написана в одном компоненте App, и режем её на компоненты. Этапов три, каждый заканчивается проверкой в консоли. В конце вы повторите то же в настоящем проекте на Vite — уже по отдельным файлам.

Этап 1вынести Header
→
Этап 2вынести Courses
→
Этап 3вынести Footer и Nav (внутри Header)
HTML на выходе остаётся тем же. Меняется только то, как собрана страница
Как проверяется «разбиение»

Итоговая разметка при разбиении не меняется — поэтому проверка смотрит ещё и на строки render …. Каждый новый компонент должен первой строкой печатать своё имя: console.log("render Header"). React вызывает компоненты по порядку «сверху вниз, вглубь», так что в консоли вы увидите порядок вызовов — и сразу поймёте, что код действительно разрезан на компоненты, а не просто переписан.

Этап 1. Вынесем Header

Перенесите кусок <header>…</header> в отдельную функцию Header и поставьте на его место <Header />.

ЗАДАЧА 7.1

Этап 1: компонент Header

средняя

Создайте компонент Header с первой строкой console.log("render Header"). В нём — вся шапка (заголовок и nav со ссылками). В App вместо шапки поставьте <Header />. Остальное не трогайте.

Страница MakeFirstStep: этап 1 · Ctrl + Enter
// TODO: объявите компонент Header

export default function App() {
  console.log("render App");
  return (
    <div className="page">
      <header className="top">
        <h1>MakeFirstStep</h1>
        <nav>
          <a href="#courses">Курсы</a>
          <a href="#contacts">Контакты</a>
        </nav>
      </header>
      <main id="courses">
        <h2>Наши курсы</h2>
        <ul>
          <li>HTML и CSS</li>
          <li>JavaScript</li>
          <li>React</li>
        </ul>
      </main>
      <footer id="contacts">Душанбе · MakeFirstStep</footer>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
render App
render Header
<div class="page">
<header class="top">
<h1>MakeFirstStep</h1>
<nav>
<a href="#courses">Курсы</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main id="courses">
<h2>Наши курсы</h2>
<ul>
<li>HTML и CSS</li>
<li>JavaScript</li>
<li>React</li>
</ul>
</main>
<footer id="contacts">Душанбе · MakeFirstStep</footer>
</div>
Подсказка 1

Образец — компоненты из раздела 02. function Header() { console.log("render Header"); return ( … ); } — в return вставьте весь тег <header> вместе с содержимым.

Подсказка 2

В App на месте шапки остаётся одна строка <Header />. Всё, что было внутри header, теперь живёт в функции Header.

Этап 2. Вынесем Courses

Теперь то же самое с блоком курсов. Он называется Courses.

ЗАДАЧА 7.2

Этап 2: компонент Courses

средняя

Header уже вынесен. Вынесите блок <main> в компонент Courses с первой строкой console.log("render Courses") и поставьте <Courses /> в App.

Страница MakeFirstStep: этап 2 · Ctrl + Enter
function Header() {
  console.log("render Header");
  return (
    <header className="top">
      <h1>MakeFirstStep</h1>
      <nav>
        <a href="#courses">Курсы</a>
        <a href="#contacts">Контакты</a>
      </nav>
    </header>
  );
}

// TODO: объявите компонент Courses

export default function App() {
  console.log("render App");
  return (
    <div className="page">
      <Header />
      <main id="courses">
        <h2>Наши курсы</h2>
        <ul>
          <li>HTML и CSS</li>
          <li>JavaScript</li>
          <li>React</li>
        </ul>
      </main>
      <footer id="contacts">Душанбе · MakeFirstStep</footer>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
render App
render Header
render Courses
<div class="page">
<header class="top">
<h1>MakeFirstStep</h1>
<nav>
<a href="#courses">Курсы</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main id="courses">
<h2>Наши курсы</h2>
<ul>
<li>HTML и CSS</li>
<li>JavaScript</li>
<li>React</li>
</ul>
</main>
<footer id="contacts">Душанбе · MakeFirstStep</footer>
</div>
Подсказка 1

Действуйте так же, как с Header: функция с большой буквы, console.log первой строкой, весь тег <main> — в return.

Подсказка 2

Порядок строк в консоли — это порядок вызова компонентов: сначала App, затем его дети по очереди — Header, потом Courses.

Этап 3. Footer и вложенный Nav

Компонент может содержать другой компонент. В шапке выделим меню в отдельный Nav — получится дерево из раздела 02.

ЗАДАЧА 7.3

Этап 3: Footer и Nav внутри Header

сложная

Вынесите <footer> в компонент Footer, а <nav> из шапки — в компонент Nav, который используется внутри Header. У каждого новый console.log("render Footer") и console.log("render Nav") первой строкой. В итоге в консоли порядок вызовов: App, Header, Nav, Courses, Footer.

Страница MakeFirstStep: этап 3 · Ctrl + Enter
// TODO: объявите Nav и Footer

function Header() {
  console.log("render Header");
  return (
    <header className="top">
      <h1>MakeFirstStep</h1>
      <nav>
        <a href="#courses">Курсы</a>
        <a href="#contacts">Контакты</a>
      </nav>
    </header>
  );
}

function Courses() {
  console.log("render Courses");
  return (
    <main id="courses">
      <h2>Наши курсы</h2>
      <ul>
        <li>HTML и CSS</li>
        <li>JavaScript</li>
        <li>React</li>
      </ul>
    </main>
  );
}

export default function App() {
  console.log("render App");
  return (
    <div className="page">
      <Header />
      <Courses />
      <footer id="contacts">Душанбе · MakeFirstStep</footer>
    </div>
  );
}

// автопроверка: печатает разметку, которую построил React
setTimeout(() => console.log(document.getElementById("root").innerHTML.replaceAll("><", ">\n<")), 100);
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
render App
render Header
render Nav
render Courses
render Footer
<div class="page">
<header class="top">
<h1>MakeFirstStep</h1>
<nav>
<a href="#courses">Курсы</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main id="courses">
<h2>Наши курсы</h2>
<ul>
<li>HTML и CSS</li>
<li>JavaScript</li>
<li>React</li>
</ul>
</main>
<footer id="contacts">Душанбе · MakeFirstStep</footer>
</div>
Подсказка 1

Footer — как Header на этапе 1. А Nav объявляется отдельно, и Header теперь содержит <h1> и <Nav />.

Подсказка 2

Порядок строк render … зависит от порядка вызова в дереве, а не от порядка объявления функций в файле.

Повторяем в проекте на Vite

Теперь то же самое «по-взрослому» — каждый компонент в своём файле. Откройте проект my-react-app из раздела 03 (или создайте новый), очистите шаблон и сделайте так:

  1. Создайте папку src/components/ и четыре файла в ней: Header.jsx, Nav.jsx, Courses.jsx, Footer.jsx.
  2. В каждый перенесите свой компонент из песочницы и допишите export default.
  3. Там, где компонент использует другой, добавьте import (Header импортирует Nav).
  4. В App.jsx импортируйте Header, Courses, Footer и соберите страницу.
  5. Стили из вкладки style.css перенесите в src/App.css и убедитесь, что App.jsx его импортирует: import './App.css'.
  6. Запустите npm run dev, откройте консоль браузера и проверьте: строки render … печатаются, и (из-за StrictMode) каждая — дважды.
src/components/Header.jsx
import Nav from './Nav.jsx'

export default function Header() {
  return (
    <header className="top">
      <h1>MakeFirstStep</h1>
      <Nav />
    </header>
  )
}
src/App.jsx
import './App.css'
import Header from './components/Header.jsx'
import Courses from './components/Courses.jsx'
import Footer from './components/Footer.jsx'

export default function App() {
  return (
    <div className="page">
      <Header />
      <Courses />
      <Footer />
    </div>
  )
}
Дерево компонентов в браузере

Установите расширение React Developer Tools для вашего браузера: в DevTools появится вкладка Components, где видно то самое дерево — App → Header → Nav и так далее. Это быстрый способ убедиться, что разбиение сделано так, как задумано.

08 Домашнее задание: страница из 5 компонентов

Соберите свою страницу на React: личную визитку, страницу кафе, афишу мероприятия — на ваш выбор. Это первый проект на React + Vite. Модуль «React Lite» — бонусный, оценка в журнал не ставится, но разбор по критериям ниже вы получите от преподавателя.

Что нужно сделать

  • ☐ Создать проект командой npm create vite@latest с шаблоном React (JavaScript), очистить шаблон: убрать счётчик, картинки и лишние стили.
  • ☐ Страница состоит минимум из 5 компонентов, каждый в своём файле в src/components/, с export default.
  • ☐ Минимум один компонент вложен в другой (например, Logo внутри Header), и минимум один используется на странице два раза и больше.
  • ☐ App.jsx только собирает страницу из компонентов: у него нет «своей» большой разметки.
  • ☐ В JSX использованы className, htmlFor (если есть форма), закрытые теги, выражения в {…} (например, год в подвале) и хотя бы один style={{…}} или условный вывод.
  • ☐ Свои стили в App.css или отдельных .css файлах; страница выглядит аккуратно и на телефоне (ширина 390 px).
  • ☐ npm run build проходит без ошибок. В репозитории нет node_modules и dist, есть package-lock.json.
  • ☐ Не менее 3 осмысленных коммитов; репозиторий на GitHub.
  • ☐ README.md: что за страница, как запустить, дерево ваших компонентов и 2–3 предложения своими словами: чем компонентный подход удобнее ручного DOM.
ИИ в домашнем задании

Правило курса: 15 минут сами. Сначала нарисуйте дерево компонентов на бумаге. Потом можно попросить ИИ: «Вот моё дерево компонентов [дерево]. Какие блоки я могу вынести в отдельные компоненты и что здесь можно переиспользовать?» — и сравнить со своим. Код, который не можете объяснить построчно, в проект не вставляйте. В README напишите, где использовали ИИ и что пришлось исправить.

Что сдавать

  • Ссылку на репозиторий GitHub в чат группы.
  • Скриншот страницы и скриншот вкладки Components (React Developer Tools) с деревом компонентов.
  • В сообщении: сколько компонентов у вас получилось и какой из них вы используете повторно.

Разбор по критериям (для самопроверки)

КритерийБаллы
Проект на Vite запускается, npm run build проходит, шаблон очищен2
5+ компонентов в отдельных файлах, верные имена, export default и import3
Есть вложенность и повторное использование компонента; App.jsx только собирает страницу2
JSX написан правильно: className, закрытые теги, выражения, один корень1
README с деревом компонентов и своими словами; аккуратные коммиты2

Баллы нужны для самопроверки: бонусный модуль не влияет на итоговую оценку курса.

✓ Шпаргалка урока

  • Ручной DOM — императив: «найди, создай, вставь, обнови». Много кода, легко забыть обновить место, разметки не видно. React — декларатив: описываем, что должно быть на экране, а React сам меняет DOM.
  • Компонент — функция, которая возвращает JSX. Имя с большой буквы, использование как тег: <Header />. Имя с маленькой буквы — это обычный HTML-тег.
  • Страница = дерево компонентов. Режем по смыслу: повторяющийся блок, блок со своей задачей. Один компонент — один файл.
  • Проект: npm create vite@latest my-app -- --template react → cd my-app → npm install → npm run dev.
  • Цепочка запуска: index.html (<div id="root">) → src/main.jsx (createRoot(…).render(<App />)) → src/App.jsx (главный компонент).
  • JSX ≠ HTML: className, htmlFor, все теги закрыты (<br />, <input />), один корень или фрагмент <>…</>, {выражение}, style={{ backgroundColor: "gold" }}, комментарий {/* … */}.
  • В {…} — только выражения: переменные, арифметика, вызовы, ? :, &&. Не рисуются true, false, null, undefined; 0 рисуется — условие n && … пишите как n > 0 && ….
  • Экспорт и импорт: export default function Header() {…} → import Header from './components/Header.jsx'. Путь с ./, расширение .jsx целиком.
  • StrictMode в режиме разработки вызывает компоненты дважды — console.log в компоненте печатается два раза. В собранном сайте так не бывает.
  • ИИ: сначала сами 15 минут; в промпте — контекст, «без useState и событий», формат «таблица строка → строка». Проверяем: имена, className, один корень, style-объект, лишние библиотеки, запуск.
  • Дальше (урок 23): props — параметры компонента, useState — его память, списки и key.