Frontend — вторая специализация продукт-инженера, и начинается она там же, где backend: со структуры, которую задают в первый день. React почти ничего не навязывает — это свобода, которая на масштабе оборачивается хаосом, если не задать правила раскладки, компонентной модели и типизации самому.

Раскладка: по фичам, а не по типам

Самая частая ошибка — раскладка по типам файлов: components/, hooks/, utils/, куда сваливается всё подряд. На десятке экранов такая структура превращается в свалку, где код одной фичи размазан по пяти папкам.

Идиоматичная альтернатива — раскладка по фичам/доменам: всё, что относится к фиче, лежит вместе.

src/
  features/
    products/
      ProductList.tsx
      ProductCard.tsx
      useProducts.ts        // загрузка данных фичи
      api.ts                // вызовы к backend
      types.ts              // типы фичи
    orders/
      ...
  shared/
    ui/                     // переиспользуемые примитивы (Button, Input)
    lib/                    // общие утилиты
  app/
    router.tsx
    App.tsx

features/ — домены, shared/ — то, что переиспользуется между ними, app/ — сборка приложения. Это та же дисциплина границ, что модули в backend-биндингах: фича не лезет во внутренности другой фичи, общее выносится в shared.

Компонент как единица

Компонент в React — функция, возвращающая разметку. Это базовая единица: один компонент — одна ответственность, один файл, имя с большой буквы.

type ProductCardProps = {
  name: string;
  price: number;
  onOpen: (id: string) => void;
};

export function ProductCard({ name, price, onOpen }: ProductCardProps) {
  return (
    <article className="product-card">
      <h3>{name}</h3>
      <p>{price} ₽</p>
    </article>
  );
}

Компонент принимает данные через props и сообщает наружу через колбэки — он не лезет за данными сам и не знает, откуда они. Это делает его переиспользуемым и тестируемым.

Типизация props

TypeScript на frontend — не формальность, а граница доверия внутри приложения. Props компонента типизируют явно (type или interface), и строгий режим (strict: true в tsconfig) включают сразу.

type ButtonProps = {
  variant: "primary" | "secondary";
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
};

Объединение строковых литералов ("primary" | "secondary") вместо string — типичный приём: компилятор не даст передать неверный вариант. Чем точнее типы props, тем меньше ошибок доходит до рантайма.

Граница UI и логики

Компонент должен оставаться про отображение. Логику — загрузку данных, вычисления, побочные эффекты — выносят в хуки (useProducts, useCart), которые компонент вызывает. Тогда UI остаётся простым, а логика тестируется отдельно.

Особая граница — стык с backend: типы ответа приходят с контракта API, и на frontend их держат в api.ts/types.ts фичи, не размазывая по компонентам. В идеале эти типы генерируются из контракта (OpenAPI), чтобы граница была типобезопасной с обеих сторон.

Где это в UCP

Структура и компонентная модель — фундамент, на который встаёт всё остальное frontend-специализации: компоненты, состояние, данные. Дисциплина та же, что в backend: чёткие границы, одна ответственность на единицу, типизированные контракты. Это то, что позволяет продукт-инженеру вести и видимую пользователю часть продукта, не утопая в хаосе по мере роста интерфейса.