Когда пишешь первый React-код, кажется, что компонент — это просто кусок HTML с логикой внутри. Со временем выясняется, что самое важное — не то, как компонент написан, а где проведены его границы. Слишком большой компонент делает слишком много и быстро превращается в нечитаемый файл на 500 строк. Слишком маленький — дробит логику так, что её сложно отследить. Разберёмся, как находить баланс.
Что такое компонент
В React компонент — это функция, которая принимает данные и возвращает разметку. Всё приложение — дерево таких функций, вложенных друг в друга.
function Greeting({ name }: { name: string }) {
return <p>Привет, {name}!</p>;
}
Компонент занимается одной вещью: принял данные — нарисовал кусок интерфейса. Как только он начинает делать несколько несвязанных вещей одновременно, это сигнал его разбить.
Где провести границу компонента
Раньше компоненты делили на два вида: «умные» (container) и «глупые» (presentational). Умный грузит данные и управляет состоянием, глупый только рисует то, что получил.
Сегодня это деление реализуют через хуки: логику выносят в отдельную функцию-хук, а компонент отвечает только за отображение.
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 — за получение данных. Логика не смешивается с разметкой.
Признак того, что компонент пора разбить: он и загружает данные, и считает что-то по ним, и рисует сложную разметку, и управляет формой — всё в одном файле.
Композиция вместо наследования
В React компоненты не наследуют друг от друга. Гибкость достигается не иерархией классов, а передачей компонентов в качестве данных.
Самый распространённый приём — проп children:
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 title="Заказ №42" actions={<button>Отменить</button>}>
<OrderDetails order={order} />
</Card>
Card не знает, что окажется внутри. Он задаёт рамку — заголовок, область действий, тело — а конкретное содержимое подставляется снаружи. Именно так строят библиотеки компонентов: один каркас, бесконечные варианты наполнения.
Render props и слоты
Когда нужна бо́льшая гибкость, чем просто children, используют несколько «слотов»:
type LayoutProps = {
sidebar: React.ReactNode;
content: React.ReactNode;
};
function Layout({ sidebar, content }: LayoutProps) {
return (
<div className="layout">
<aside>{sidebar}</aside>
<main>{content}</main>
</div>
);
}
Таким образом разные части интерфейса подставляются независимо, не вынуждая создавать десятки вариантов одного и того же компонента.
Props и state: кто чем владеет
Это различие — основа React:
- Props — данные, которые компонент получает снаружи. Он их не меняет.
- State — данные, которыми компонент управляет сам.
type ToggleProps = {
on: boolean;
onChange: (on: boolean) => void;
};
export function Toggle({ on, onChange }: ToggleProps) {
return (
<button onClick={() => onChange(!on)}>
{on ? "Включено" : "Выключено"}
</button>
);
}
Здесь Toggle не хранит своё состояние — он получает значение через props и сообщает об изменении через колбэк. Такой компонент называют контролируемым: им управляет родитель.
Когда вносить state прямо внутрь компонента:
function Dropdown({ options }: { options: string[] }) {
const [open, setOpen] = React.useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Выбрать</button>
{open && (
<ul>
{options.map((o) => <li key={o}>{o}</li>)}
</ul>
)}
</div>
);
}
open — локальное состояние Dropdown. Оно не нужно нигде выше, родителю незачем знать, открыт дропдаун или нет. Если же состояние нужно нескольким компонентам — его поднимают вверх к их общему родителю.
Переиспользование без лишнего усложнения
Желание сразу написать универсальный компонент «на все случаи» — ловушка. Компонент с десятком пропов под разные режимы становится труднее читать, чем два простых отдельных компонента.
Разумный подход: сначала написать конкретное решение в том месте, где оно нужно. Когда тот же компонент понадобится во втором-третьем месте и его форма устоится — вынести в общую папку shared/ui.
shared/
ui/
Button/
Card/
Modal/
feature/
orders/
OrderCard/ ← живёт здесь, пока не понадобится в другом месте
Преждевременное обобщение так же вредно, как дублирование кода.
Коротко
- Компонент — функция, которая принимает данные и возвращает разметку. Одна ответственность.
- Логику выносят в хуки, компонент занимается отображением.
- React использует композицию:
childrenи props-слоты вместо наследования классов. - Props — данные от родителя, компонент их не меняет. State — данные, которыми владеет сам компонент.
- Если состояние нужно нескольким компонентам — его поднимают к общему родителю.
- Общий компонент выносят тогда, когда повторение проявилось, а форма ясна — не раньше.
Что почитать дальше
- Состояние и управление данными — куда и как поднимать состояние, когда нужен глобальный store.
- TypeScript в React — типизация props, generics, дискриминированные union.
- Структура проекта — как раскладывать компоненты по папкам по мере роста приложения.