Компонентный подход в вёрстке: типичные ошибки
Компоненты обещают переиспользование, но на практике превращаются в кашу из пропсов и утечек стилей. Разбираем ошибки, которые ломают компонентную архитектуру, и как их избежать.

Компонент, который начинался как аккуратная кнопка на 20 строк, через полгода принимает 14 пропсов, имеет три булевых флага-режима и внутри разветвляется на четыре разных внешних вида. Каждое изменение ломает что-то в соседнем месте, потому что кнопку используют в 60 файлах. Это не проблема React, Vue или Web Components — это проблема границ компонента, которые провели неправильно. Разбираем типичные ошибки компонентной вёрстки и цену каждой из них.
Ошибка 1. Компонент знает про своё окружение
Самый частый способ убить переиспользование — зашить в компонент внешние отступы. Кнопка с margin-top: 16px внутри своих стилей ломается в первом же месте, где её нужно поставить без отступа.
/* Плохо: компонент управляет расстоянием до соседей */
.card {
margin-bottom: 24px;
margin-left: 16px;
}
Отступ между элементами — свойство контейнера, а не самих элементов. Компонент отвечает за то, что внутри его границы. За расстояние до соседа отвечает родитель — через gap, flex или grid.
/* Хорошо: расстоянием управляет контейнер */
.card-list {
display: grid;
gap: 24px;
}
.card {
/* никаких внешних margin */
padding: 16px;
border-radius: 8px;
}
Цена ошибки: каждый новый контекст использования требует переопределять или обнулять margin, растёт число костылей вида .card--no-margin. При gap у родителя таких костылей нет вообще.
Ошибка 2. Пропс-флаги вместо отдельных компонентов
Когда компонент обрастает булевыми пропсами isPrimary, isDanger, isGhost, isLarge, внутри него появляется дерево условий. Это первый признак, что в одном компоненте живут несколько разных.
Флаги комбинируются, и большинство комбинаций не имеет смысла: isPrimary + isGhost — что должно нарисоваться? Компонент вынужден иметь мнение о невалидных состояниях, и оно всегда неполное.
Если у компонента больше двух булевых пропсов, отвечающих за внешний вид, вы почти наверняка склеили два-три компонента в один и платите за это на каждом ревью.
Решение — заменить набор флагов одним пропсом-перечислением. Вместо isPrimary, isDanger — один variant: 'primary' | 'danger' | 'ghost'. Так невалидная комбинация становится невыразимой в принципе.
Ошибка 3. Стили протекают наружу
Компонент, который стилизует теги по имени (button, a, ul) без изоляции, начинает влиять на всё, что окажется внутри него по вложенности. Достаточно вставить внутрь карточки чужой список — и он получит чужие отступы.
Способы изоляции зависят от инструмента:
- CSS Modules — имена классов хешируются при сборке, глобального имени не остаётся;
- Scoped-стили во Vue (атрибут
scopedу<style>) — селекторы получают уникальный атрибут компонента; - Shadow DOM в Web Components — стили не пересекают границу теневого дерева вообще;
- методология БЭМ — изоляция не техническая, а дисциплинарная: уникальный префикс блока в имени класса.
Общее правило независимо от инструмента: селектор компонента не должен цепляться за голые теги. Стилизуйте свой класс, а не div внутри.
Ошибка 4. Фиксированная ширина и высота вместо контента
Жёсткие width: 320px и height: 200px на компоненте выглядят нормально на макете и разваливаются на первом же длинном тексте или переводе интерфейса на немецкий. Текст либо обрезается, либо вылезает за границы.
Компонент должен подстраиваться под содержимое и под контейнер, а не диктовать абсолютный размер. Вместо фиксированной высоты — min-height и padding, которые дают компоненту расти. Вместо фиксированной ширины — max-width плюс поведение внутри флекс- или грид-родителя.
Ошибка 5. Дублирование вместо композиции
Появилась карточка товара — сделали ProductCard. Понадобилась карточка со скидкой — скопировали в ProductCardDiscount. Понадобилась карточка в избранном — ещё одна копия. Три компонента, которые на 80% совпадают, расходятся при первой же правке общей части.
Признак, что пора выделять общее: вы правите одно и то же в нескольких файлах подряд. Общая оболочка выносится в базовый компонент, а различия передаются через слоты (children в React, <slot> во Vue и Web Components).
Композиция через слот, а не через пропс
Когда внутрь компонента нужно вставить произвольную разметку, соблазн — передать её строкой в пропс. Это ломает изоляцию и открывает дыру для XSS, если строку вставлять через innerHTML. Слот решает ту же задачу без разбора HTML руками.
<!-- Vue 3.x: базовая карточка со слотами -->
<template>
<div class="card">
<header class="card__head">
<slot name="head"></slot>
</header>
<div class="card__body">
<slot></slot>
</div>
</div>
</template>
Как отличить хороший компонент от плохого
| Признак | Здоровый компонент | Проблемный компонент |
|---|---|---|
| Пропсы внешнего вида | Один enum-вариант | Пять булевых флагов |
| Внешние отступы | Нет, задаёт родитель | margin внутри стилей |
| Размеры | min-height, max-width | Фиксированные px |
| Стили | Изолированы, свой класс | Цепляются за голые теги |
| Похожие варианты | Композиция через слоты | Скопированные файлы |
Когда компонент дробить не нужно
Обратная крайность тоже вредит: разбиение на компоненты ради самого разбиения. Обёртка из одного div с одним слотом внутри не даёт ничего, кроме лишнего файла и лишнего уровня в дереве. Дробите, когда есть переиспользование или когда часть разметки живёт своей логикой. Если фрагмент используется в одном месте и не имеет собственного состояния — он может остаться частью родителя.
Что делать дальше
Пройдитесь по своей библиотеке компонентов с чек-листом из таблицы выше. Начните с самого часто используемого компонента — как правило, это кнопка или карточка: именно там флаги и внешние margin наносят больше всего урона. Если в проекте есть Storybook или аналог, соберите на нём все варианты одного компонента на одном экране — комбинации, которые невозможно отрисовать осмысленно, покажут, где флаги стоило заменить на enum. Следующий шаг после наведения порядка в границах — договориться о единой системе отступов (spacing scale), чтобы gap у родителей брались из общего набора значений, а не из головы.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: MDN: Using slots in Web Components, Vue 3 Docs: Slots
Частые вопросы
Это касается только React или любого фреймворка?
А если у меня уже десятки компонентов с margin внутри — всё переписывать?
Сколько пропсов для компонента — это уже много?
Что делать, если после замены margin на gap поехала вёрстка?
Слоты работают одинаково во всех фреймворках?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.