React: компоненты, JSX и композиция
Оглавление · JavaScript · TypeScript
Задача: представить события как дерево интерфейса, не смешивая загрузку контента с разметкой. Нужны функции, массивы и объектные типы. Примеры учебные; они не заменяют существующий UIKit.
От значений к дереву
В простейшей модели компонент вычисляет описание интерфейса по входным данным:
view: Props → Tree. Это JSX-дерево, не готовая строка HTML и не команда
«добавить элемент в DOM». React использует описание, чтобы отобразить интерфейс.
У интерактивного компонента в модель войдёт также состояние.
HTML задаёт смысл элементов: section группирует раздел, h2 — его заголовок,
ul — список, li — элемент списка, p — абзац, strong — смысловое выделение.
JSX похож на HTML, но позволяет вставлять значения JavaScript и компоненты.
Синтаксис JSX.
import type { ReactNode } from "react";
export type LessonEvent = {
id: string;
title: string;
location: string;
};
export function EventList({ entries }: { entries: readonly LessonEvent[] }) {
if (entries.length === 0) return <p>На этой площадке пока нет событий.</p>;
return (
<ul>
{entries.map((entry) => (
<li key={entry.id}>
<strong>{entry.title}</strong> — {entry.location}
</li>
))}
</ul>
);
}
export function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
export const fixtures: readonly LessonEvent[] = [
{ id: "opening", title: "Открытие", location: "Площадь" },
{ id: "workshop", title: "Мастерская", location: "Шатёр" },
];
export function BasicExample() {
return (
<Section title="Учебная программа">
<EventList entries={fixtures} />
</Section>
);
}
Это полный учебный модуль. Сохраните его как временный
apps/docs/src/react-components.tsx в своей ветке и проверьте через
pnpm --filter @atmanki/docs typecheck. Node напрямую TSX не исполняет:
для показа нужен React через существующий dev-сайт или Storybook.
Имена компонентов начинаются с заглавной буквы: <EventList /> означает
компонент, <ul> — встроенный HTML-элемент. В фигурных скобках JSX находится
выражение: массив, строка или результат вызова. Вне JSX фигурные скобки
могут обозначать блок функции или объект — это другие конструкции языка.
Атрибут класса пишется className, связь label с полем — htmlFor.
Теги закрываются, включая одиночные <input />. Несколько соседних элементов
можно объединить фрагментом <>...</>, не создавая дополнительный DOM-узел.
Props — явный вход
entries приходит от родителя; компонент не обращается к CMS.
Деструктуризация параметра читает поле объекта props. Поля могут содержать
массивы, функции и React-узлы. Их нельзя мутировать при рендере.
Передача props.
Исходник схемы
flowchart TD Parent["BasicExample"] --> Section["Section: title и children"] Section --> List["EventList: entries"] List --> First["li: opening"] List --> Second["li: workshop"]
Стрелки показывают вложенность интерфейса, не импорты модулей и не сетевые
запросы. children — переданное содержимое между открывающим и закрывающим
тегами компонента. Здесь Section не знает устройство списка; он отвечает
за общую рамку раздела. Это композиция по ответственности.
Компоненты объявляем на уровне модуля и используем через JSX. Прямой вызов
EventList({ entries }) лишает React обычной границы компонента;
после добавления Hooks такой подход особенно опасен. Определение компонента
внутри другого компонента также создаёт новую функцию при каждом рендере
и может неожиданно сбрасывать состояние вложенного дерева.
Список и идентичность
map превращает каждое событие в элемент описания. key связывает элемент
со стабильным идентификатором среди соседей. Он нужен при вставке, удалении
и перестановке, чтобы React сопоставлял элементы между рендерами.
Ключи списков.
Индекс массива описывает положение, не событие. Для переставляемого списка
он может связать локальное состояние строки с другой записью.
Случайный ключ при каждом рендере делает элементы каждый раз новыми.
Заголовок тоже плохой идентификатор: он меняется и может повторяться.
key — служебная информация React; компонент не получает его обычным prop.
Если нужен ID внутри компонента, передайте id отдельно.
В примере у каждой записи есть уникальный id. Существование такого поля
в типе не доказывает уникальность данных: это отдельный контракт коллекции.
Условие и пустой результат
if выбирает ранний возврат; condition ? A : B выбирает выражение внутри JSX.
Для скрытого содержимого компонент может вернуть null.
Выражение entries.length && <EventList ... /> при пустом списке даёт число
0, которое React может показать. Явное entries.length > 0 выражает
нужный boolean-предикат.
Пустые данные и отказ загрузки должны иметь разные представления.
EventList здесь знает только успешно полученный список. Передавать в него []
при ошибке источника значило бы скрыть отказ под видом отсутствия событий.
Эти варианты вводились в ADT.
Рендер должен оставаться вычислением
Не отправляйте HTTP-запрос и не меняйте переданный массив во время рендера. React может повторно вызвать вычисление дерева; число рендеров не является числом пользовательских действий. Сортируйте копию, если порядок нужно менять. Чистота компонентов.
Рендер, применение изменений к DOM и эффекты — разные этапы. Их подробности разберём после событий и состояния. Отсутствие DOM-мутации в вашем коде не делает рендер чистым, если он меняет глобальный объект или читает неявно меняющийся вход.
Применение в Atmanki
ArticleCard получает данные и renderLink.
UIKit не выбирает Next-роутер: приложение передаёт нужный способ построения ссылки.
Faq получает ответы как ReactNode; он отвечает
за Accordion, а не за загрузку CMS. Главная страница
загружает данные и передаёт их компонентам.
Нативные HTML-элементы выше помогают увидеть механику React. В рабочих формах и интерфейсных примитивах проекта используйте Mantine напрямую и существующий UIKit. Новая обёртка нужна для самостоятельной ответственности, а не чтобы переименовать props Button.
Практика и самопроверка
Создайте временный файл apps/docs/src/app/lesson/page.tsx:
import { BasicExample } from "@/react-components";
export default function LessonPage() {
return <BasicExample />;
}
Из корня worktree запустите pnpm docs:dev и откройте
http://localhost:3002/lesson/. Ожидаются заголовок «Учебная программа» и две записи.
Проверьте пустой список и перестановку исходных записей.
Заголовки выводятся как текст: строка <b>Открытие</b> не должна стать HTML-разметкой.
Не применяйте dangerouslySetInnerHTML для показа такого заголовка.
Измените Section так, чтобы рядом с заголовком можно было передать ReactNode
action. Родитель задаёт действие; Section не выбирает URL и не загружает данные.
Проверьте отсутствие action и обычную ссылку. После упражнения удалите временные
файлы и восстановите учебную страницу.
Объясните, какие данные принадлежат родителю, зачем здесь children и почему ключ списка не равен индексу или заголовку.