Учебник веб-разработки
Разделы учебника
На этой странице

IV. React и состояние

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.

Практика и самопроверка

Добавьте EventList в временную учебную страницу docs или отдельную локальную story. Проверьте две записи, пустой список и перестановку исходных записей. Заголовки выводятся как текст: строка <b>Открытие</b> не должна стать HTML-разметкой. Не применяйте dangerouslySetInnerHTML для показа такого заголовка.

Измените Section так, чтобы рядом с заголовком можно было передать ReactNode action. Родитель задаёт действие; Section не выбирает URL и не загружает данные. Проверьте отсутствие action и обычную ссылку. После упражнения удалите временные файлы и восстановите учебную страницу.

Объясните, какие данные принадлежат родителю, зачем здесь children и почему ключ списка не равен индексу или заголовку.