Компонент — единица frontend, как Handler единица сценария в backend. И главный вопрос тот же: где провести границу. Слишком крупный компонент знает слишком много; слишком мелкий — дробит логику в пыль. Хорошая компонентная модель держит баланс через композицию и чёткое разделение «что показывать» и «как это работает».

Граница и ответственность

У компонента одна ответственность. Признак нарушенной границы — компонент, который и грузит данные, и считает бизнес-логику, и рисует сложную разметку, и управляет формой. Такой разбивают: отображение остаётся в компоненте, логика уходит в хук.

function useProductList() {
  const { data, isLoading } = useProducts();
  return { products: data ?? [], isLoading };
}

export function ProductList() {
  const { products, isLoading } = useProductList();
  if (isLoading) return <Spinner />;
  return (
    <ul>
      {products.map((p) => (
        <ProductCard key={p.id} {...p} />
      ))}
    </ul>
  );
}

Компонент ProductList отвечает за вид; useProductList — за получение данных. Разделение «представление / логика» (через хуки) пришло на смену старому делению на presentational/container-компоненты, но цель та же: UI остаётся простым.

Композиция вместо наследования

В React компоненты не наследуют — их компонуют. Гибкость даёт не иерархия классов, а передача узлов через children и props-слоты.

type CardProps = {
  title: string;
  actions?: React.ReactNode;
  children: React.ReactNode;
};

export function Card({ title, actions, children }: CardProps) {
  return (
    <section className="card">
      <header>
        <h3>{title}</h3>
        {actions}
      </header>
      <div className="card-body">{children}</div>
    </section>
  );
}

Card не знает, что внутри, — он задаёт рамку, а содержимое и действия передают снаружи. Это композиция: общий каркас плюс подставляемые части. Тот же принцип «композиция вместо наследования», что и в backend-дизайне, только средствами React.

Props против состояния

Чёткое различие, которое экономит много багов: props — данные, приходящие снаружи (компонент их не меняет); state — данные, которыми компонент владеет сам. Если значение приходит сверху и компонент его меняет у себя — это запах: либо оно должно быть state, либо изменение должно уходить наверх колбэком.

type ToggleProps = {
  on: boolean;              // props: владелец — родитель
  onChange: (on: boolean) => void;
};

export function Toggle({ on, onChange }: ToggleProps) {
  return <button onClick={() => onChange(!on)}>{on ? "On" : "Off"}</button>;
}

Здесь on — props (контролируемый компонент), а решение об изменении уходит наверх. Кто владеет состоянием — отдельная большая тема следующей статьи.

Переиспользование без дублирования

Выносить компонент в общий (shared/ui) стоит, когда он реально нужен в двух-трёх местах и его роль устоялась. Преждевременное обобщение так же вредно, как дублирование: общий компонент с десятком props под все случаи становится нечитаемым. Правило простое — сначала продублировать локально, обобщить, когда повторение проявилось и форма ясна.

Где это в UCP

Компонентная модель — это дисциплина границ на frontend: одна ответственность, композиция вместо наследования, явное различие props/state. Это те же инженерные принципы, что держат backend-код чистым, только применённые к UI. Владея ими, продукт-инженер строит интерфейс, который растёт без превращения в запутанный клубок, — а данные, которыми компоненты живут, разбираются в статье про состояние.