← назад к разделу

Когда пишешь первый 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.
  • Структура проекта — как раскладывать компоненты по папкам по мере роста приложения.