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: чёткие границы, одна ответственность на единицу, типизированные контракты. Это то, что позволяет продукт-инженеру вести и видимую пользователю часть продукта, не утопая в хаосе по мере роста интерфейса.