Компонент — единица 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. Владея ими, продукт-инженер строит интерфейс, который растёт без превращения в запутанный клубок, — а данные, которыми компоненты живут, разбираются в статье про состояние.