19 июн. 2025 г.·6 мин чтения

Границы дизайн‑системы, которые останавливают «prop soup» на раннем этапе

Узнайте, как границы дизайн‑системы сохраняют компоненты простыми, переносят бизнес‑правила в фичи и предотвращают рост «prop soup» до того, как переиспользование начнёт ломаться.

Границы дизайн‑системы, которые останавливают «prop soup» на раннем этапе

Как выглядит «prop soup"

Путаница с пропсами начинается незаметно. Общий компонент стартует с нескольких понятных опций, а затем постепенно превращается в мешок переключателей, исключений и одноразовых настроек. То, что раньше казалось простым, теперь требует длинного списка пропсов просто чтобы отрисовать один экран.

Вы замечаете это, когда кто‑то открывает компонент и тратит больше времени на чтение его API, чем на использование. Кнопка, у которой раньше были size, variant и disabled, теперь требует isCompact, withIcon, iconSide, iconOnly, loadingText, showSpinner, useBrandColor и трёх пропсов, которым никто полностью не доверяет.

Большинство команд не создают этот бардак намеренно. Он растёт по одному запросу. Для страницы продаж нужен особый стиль. В админке нужна дополнительная валидация. В одном клиентском сценарии нужен предупреждающий статус, который появляется только в узком случае. Никто не хочет форкать компонент, поэтому добавляют ещё один проп и идут дальше.

Через несколько месяцев имена перестают иметь смысл. Появляются пары вроде compact и dense, и никто не может объяснить разницу. Появляются флаги, которые входят в противоречие, например isPrimary вместе с variant="secondary", или hideFooter рядом с footerActions. Компонент всё ещё работает, но никто не может угадать поведение без теста.

Чаще всего это значит, что граница смещена. Компонент перестаёт отвечать только за представление. Он начинает нести бизнес‑правила, правила страницы и старые решения, которые логичнее держать в другом месте.

Переиспользование замедляется по мере роста API. Люди перестают спрашивать «Подходит ли это для моего экрана?» и начинают спрашивать «Какая комбинация пропсов даёт наименее сломанный вариант?». Маленькие изменения занимают больше времени, потому что каждая новая опция может сломать старый кейс. Код‑ревью тоже замедляются. Коллегам приходится спрашивать, отражает ли новый проп реальную дизайнерскую необходимость или это просто заплатка.

В этот момент общий компонент уже не экономит времени. Он собирает путаницу.

Почему общие компоненты портятся

Большинство общих компонентов не портятся из‑за плохого планирования. Они портятся, потому что быстрое решение кажется маленьким. Команда продукта укладывается в дедлайн, видит компонент, который почти подходит, и добавляет ещё один проп, чтобы покрыть экран. Это экономит время сегодня, поэтому никто не возражает.

Проблема начинается, когда это правило переезжает в библиотеку, а не остаётся в продуктовом коде. Модал стал знать о биллинге. Таблица стала заботиться о том, на триал ли пользователь. Поле формы меняет поведение для одного админского потока. Каждое решение кажется безвредным по‑отдельности.

Потом люди копируют паттерн. Одно исключение принимается, и следующая команда добавляет ещё одно. Скоро API компонента несёт кусочки онбординга, прав доступа, ценообразования и отчётности. Командa всё ещё называет это переиспользуемым, но он переиспользуем только если все принимают одинаковую гору особых случаев.

Простой пример с кнопкой показывает, как это происходит. Сначала несколько понятных опций: размер, тон и состояние disabled. Позже одна команда добавляет проп для текста загрузки в checkout. Другая добавляет иконку‑замок для ограниченных действий. Третья — поведение трекинга для кампании. Кнопка всё ещё выглядит как один общий компонент, но внутри неё живут бизнес‑правила из трёх разных частей продукта.

Это вредит переиспользованию. Компонент пытается обслужить множество экранов, но каждый — немного плохо. Простые экраны тащат лишние опции, которые им не нужны. Новым экранам всё ещё нужны костыли, потому что старые пропсы были созданы для чужого кейса. API растёт, намерение размывается.

Стоимость тестирования обычно делает гниение очевидным. Небольшое изменение в раскладке, валидации или поведении кнопки теперь затрагивает настройки, checkout, админские инструменты и маркетинговые потоки. Команды повторно тестируют гораздо больше UI, чем этого требует изменение. Они также работают медленнее, потому что никто не уверен, от какой комбинации пропсов зависит другой экран.

Здоровая библиотека даёт простые строительные блоки. Когда общий компонент начинает знать слишком много о тарифах, ролях или логике страницы, он пересёк границу.

Где должны жить бизнес‑правила

Общие компоненты должны решать UI‑задачи, но не политику продукта. Кнопка может знать свой размер, стиль, состояние загрузки и disabled. Она не должна знать, кто может одобрить скидку, какой план открывает функцию или доступен ли шаг 4 до шага 3.

Эта граница важна, потому что дизайн‑система должна владеть структурой, визуальными состояниями, отступами и поведением в плане доступности. Она может открывать общие входы вроде disabled, selected, error или loading. Эти пропсы описывают, что UI должен показывать, а не почему продукт выбрал это состояние.

Хороший общий компонент отвечает на узкий набор вопросов: как это должно выглядеть, в каком оно состоянии, как реагирует на ввод и какое содержимое отображает?

Правила ценообразования, проверки прав и workflow‑правила принадлежат коду фич. Там продукт уже знает тип аккаунта, роль пользователя, текущий шаг, статус оплаты или состояние одобрения. Код фичи может принять решение, а затем передать результат вниз как обычные UI‑пропсы.

Обёртки здесь очень помогают. Они позволяют командe продукта держать бизнес‑логику рядом с фичей и при этом использовать те же общие строительные блоки внизу.

Например, у вас есть общий Button в дизайн‑системе. Блок биллинга может создать UpgradePlanButton‑обёртку, которая проверяет план клиента, количество мест и статус оплаты. После выполнения этой логики она рендерит общий Button с меткой, состоянием disabled и обработчиком клика. Общая кнопка остаётся чистой. Биллинг‑логика остаётся там, где ей место.

Это также сохраняет API небольшим. Как только компонент начинает собирать пропсы вроде planTier, userRole, canApprove, requiresManager или hideForGuests, он перестаёт быть по‑настоящему общим. Он становится продуктовым компонентом в обёртке дизайн‑системы.

Маленькие API легче читать и легче доверять. Разработчик может открыть компонент, просканировать пять‑шесть пропсов и быстро понять суть. Это экономит время на ревью и снижает количество случайных ошибок.

Когда вы чувствуете давление добавить ещё один бизнес‑проп, остановитесь и поднимитесь на уровень выше. Поставьте правило в обёртке, контейнере или в логике фичи. Пусть общий компонент останется скучным. Скучные компоненты стареют хорошо.

Простой способ провести границу

Возьмите один компонент, который уже раздражает в использовании. Не самый худший в кодовой базе, просто тот, к которому люди боятся подойти. Когда компонент требует длинного списка пропсов и половина команды не помнит, что делает каждый флаг, граница уже размыта.

Запишите все пропсы, которые он принимает. Затем отсортируйте их в несколько простых групп: визуальные пропсы, которые меняют внешний вид; интерактивные — как пользователь взаимодействует; бизнес‑пропы, которые отражают правила продукта или состояние workflow; расплывчатые имена, которые вызывают вопрос; и конфликтующие пропсы, которые никогда не должны появляться вместе.

Этот небольшой аудит обычно быстро показывает проблему. Цвет, размер, отступы и позиция иконки — в зоне общего компонента. Обработка клика, состояние открытия и поведение клавиатуры часто тоже там. Но если проп меняет, кто может что‑то одобрить, показывает ли пользователь цену или какой статус разблокирует действие, — эта логика должна уйти.

Обёртка обычно самое чистое решение. Оставьте общий компонент простым, затем создайте продуктовую обёртку, которая подаёт нужные значения. Например, общий Button может управлять стилем и состоянием disabled. ApproveInvoiceButton‑обёртка может решать, когда разрешено одобрение, какой текст показать и какое событие отправить. Общая часть остаётся переиспользуемой. Бизнес‑часть — читаемой.

Имена важнее, чем многие команды признают. Пропсы вроде mode, variant2, type или special скрывают намерение. Переименуйте их так, чтобы другой разработчик мог угадать результат без открытия файла компонента. showWarningIcon понятнее, чем alertStyle. canEdit понятнее, чем editable, когда доступ решает приложение.

Затем уберите плохие комбинации. Если isArchived и canSubmit никогда не должны быть true одновременно, не оставляйте этот выбор на усмотрение каждого вызывающего. Разделите API, добавьте обёртку или уберите один проп. Хорошие границы дизайн‑системы кажутся скучными в лучшем смысле — люди используют компонент без чтения длинной заметки или запоминания дюжины исключений.

Один пример из продуктовой команды

Refactor Without Breaking Screens
Map a safe cleanup path that keeps old screens working while you move logic out.

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

Потом появляются реальные правила продаж.

Промо‑код работает для одного продукта, но не для другого. Налог зависит от страны. В некоторых регионах нужен почтовый индекс перед расчётом суммы. Бизнес‑клиентам нужен налоговый идентификатор, но только в некоторых рынках. Простая форма превращается в движок правил с множеством входов.

Список пропсов растёт быстро: country, region, discountType, discountValue, taxMode, showTaxId, requireCompanyName, allowGuestCheckout, recalculateOnBlur. Новый разработчик открывает компонент и не понимает, какие пропсы можно безопасно комбинировать. Изменение для Германии ломает Канаду. Другая фикса помогает подпискам, но вредит одноразовым платежам. Со временем никто не доверяет общему компоненту.

Проблема не в форме. Проблема в том, что правила checkout переехали в слой общей библиотеки.

Здоровее разделение проще. Фича checkout решает, какие поля показывать, применяет скидки и налоговые правила и выбирает нужную валидацию. Общая форма отвечает за раскладку, поля, отображение ошибок и фокус. Она не знает о политике ценообразования и региональных правилах.

Это обычно означает добавление контейнера фичи. Контейнер читает корзину, тип клиента и регион. Он вычисляет итог, выбирает валидацию и передаёт простые UI‑пропсы в общую форму. Вместо того чтобы просить форму разбираться во всех кейсах оформления, команда просит её делать одну вещь хорошо: чётко показывать форму и корректно обрабатывать ввод.

Именно здесь границы дизайн‑системы экономят время. Общие компоненты решают, как вещи выглядят и ведут себя на экране. Код фичи решает, что продукт разрешает.

Когда команды проводят такое разделение рано, переиспользование остаётся здоровым. Общая форма остаётся скучной в лучшем смысле. А скучные компоненты редко удивляют на неделе релиза.

Как исправить запутанный компонент

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

Запутанный компонент редко требует полного переписывания. Большинство команд выигрывают от медленной очистки, которая защищает старые экраны, пока логику переносят наружу.

Простой путь очистки работает так:

  1. Заморозьте новые пропсы на короткий период. Обращайтесь с текущим API как с наследием, хотя бы на один спринт.
  2. Добавьте тонкую обёртку для старых экранов. Обёртка может переводить старые пропсы в более простую форму, которую вы хотите оставить.
  3. Переносите правила по одному. Выносите логику ценообразования, проверки прав или онбординга в экран или в хук фичи, а не в общий компонент.
  4. Напишите несколько примеров, которые показывают новую границу. Делайте их небольшими и реалистичными, чтобы другие команды могли копировать.
  5. Удаляйте мёртвые пропсы после того, как команды переключатся. Если никто больше не использует проп, удаляйте его, а не держите «на всякий случай».

Эта тонкая обёртка важнее, чем многие думают. Она позволяет улучшать API компонента, не ломая половину продукта. Общий компонент сразу становится меньше, а старые страницы продолжают работать, пока у каждой команды не дойдёт очередь обновиться.

Например, если у общего PlanCard были пропсы isEnterprise, showUpgradeWarning, canStartTrial и regionNotice, значит компонент делает слишком много. Оставьте карточку за раскладкой, слотами текста и визуальными состояниями. Пусть страница биллинга решает, кто увидит предупреждение и какое уведомление показать.

Примеры помогают закрепить новую форму. Напишите два‑три небольших кейса в документации, тестах или story‑файлах: простая карточка, карточка с бейджем и обёртка страницы биллинга, которая добавляет бизнес‑правила вне общего слоя. Люди копируют примеры чаще, чем читают правила.

Очистка не закончена, когда новый API выпустили. Она закончена, когда старые пропсы удалены. Если вы оставите мёртвые пропсы, следующий дедлайн снова вернёт тот же бардак.

Ошибки, которые возвращают бардак

Fix Prop Soup Early
Bring in Fractional CTO support to fix messy component APIs without a full rewrite.

Команды редко разрушают компонент одним большим шагом. Они делают это маленькими, поспешными правками. Приходит дедлайн, кто‑то добавляет ещё один булев, и компонент теперь должен обрабатывать новое правило, которому не место в нём.

Так проп‑суп возвращается. Чистая кнопка, карточка или обёртка формы превращается в место, где скапливаются продуктовые решения, правила ценообразования, проверки прав и странные одноразовые кейсы.

Типичные ошибки повторяются. Флаг добавляют только для одного релиза, затем оставляют. Кто‑то прячет логику за расплывчатыми именами вроде mode, variant или context. Презентационная часть начинает сама загружать данные, поэтому простой UI‑блок теперь знает об API, состояниях загрузки, правилах повтора и пустых результатах. Редкий кейс становится дефолтным, потому что никто не сделал обёртку для специального потока. Старые пропсы никогда не уходят, и новые люди продолжают спрашивать, что они делают и можно ли их трогать.

Типичный пример — сводка по чекауту, которая начиналась как небольшой компонент отображения. Позже туда добавляют isEnterprise, hasPastDueInvoice, fetchTaxRate и showRetentionOffer. Ни одно из этих идей не принадлежит одному и тому же слою. Компонент всё ещё рендерит, но никто не может предсказать, какие комбинации пропсов имеют смысл.

Исправление обычно скучное, и это хороший знак. Поместите загрузку данных в контейнер или хук страницы. Поместите бизнес‑правила в слой фич. Держите общий компонент сосредоточенным на рендеринге понятных входов: текста, цены, статуса и действий.

Команды также должны обладать смелостью удалять. Если проп не используется, удалите его. Если один экран требует кастомного поведения, оберните общий компонент вместо того, чтобы учить базовый компонент новой трюковой функции. Эта привычка сохраняет переиспользование здоровым и не даёт тому же бардаку вернуться через месяц.

Быстрая проверка перед тем как добавить ещё один проп

Draw Cleaner Boundaries
Oleg can help your team separate UI concerns from product rules.

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

Хороший первый фильтр прост: этот проп меняет внешний вид компонента, или он внедряет политику продукта? Размер, тон, отступы и позиция иконки обычно относятся к компоненту. Правила вроде «показывать только платным пользователям» или «отключить после трёх неудачных попыток» — не относятся. Эти правила должны жить в продуктовом слое, часто в обёртке или логике экрана.

Перед добавлением спросите прямо:

  • Поймёт ли другую команда имя этого пропа за десять секунд?
  • Описывает ли он визуальное изменение, а не правило продукта?
  • Может ли он конфликтовать с другим пропом и создавать странные состояния?
  • Решит ли задачу мелкая обёртка без изменения библиотеки?
  • Хотели бы вы поддерживать этот API через год?

Третий вопрос экономит много проблем. Если isCompact и showFullMetadata могут быть одновременно true, что произойдёт? Если variant="primary" и isDanger оба пытаются управлять цветом, кто выиграет? Как только два пропса могут сражаться, люди начинают догадываться. Догадки — это как общий компонент превращается в проп‑суп.

Обёртки часто чище. Допустим, биллинг‑команде нужна кнопка с иконкой‑замком для неоплаченных аккаунтов, которая открывает поток апгрейда по клику. Дизайн‑системная кнопка не должна учить биллингу правила. UpgradeButton‑обёртка может решать текст, иконку, состояние disabled и действие, в то время как базовая кнопка остаётся простой.

Последняя проверка — поддержка. Каждый новый проп становится частью обещания, которое вы даёте другим командам. Если имя кажется узким, временным или привязанным к одному экрану, вероятно, ему не место в общем компоненте. Именно так границы дизайн‑системы сохраняют переиспользование здоровым: базовые части остаются простыми, а продукт‑поведение остаётся рядом с продуктом.

Что делать дальше

Начните с одного компонента, на который все жалуются. Выберите тот, у которого слишком много пропсов, слишком много условий и слишком много исключений. Если кнопка, карточка или таблица имеют смысл только после долгого Slack‑потока — это хороший кандидат для аудита.

Посмотрите на компонент в реальном использовании, а не в изоляции. Проверьте, сколько пропсов существуют только потому, что одна продуктовая область нуждалась в особом правиле. Именно здесь обычно и соскользнула граница. Общий компонент должен отвечать за структуру, визуалы и базовое поведение. Код фичи должен решать бизнес‑правила.

Короткое рабочее правило помогает больше, чем длинный документ: дизайн‑система владеет раскладкой, токенами, состояниями и доступностью; модули фич владеют правами, логикой ценообразования, eligibility и продуктовой копией; обёртки соединяют оба мира, когда флоу продукта требует и того и другого; и если проп существует только ради одного экрана, вероятно, ему не место в общем компоненте.

Потом просмотрите обёртки и модули фич с командой. Откройте несколько реальных файлов и задавайте простые вопросы. Эта логика принадлежит продуктовому потоку? Проп появился потому, что базовому компоненту не хватало визуальной опции или потому, что в него просочилась продуктовая логика? Многие споры можно разрешить за 20 минут, когда код открыт на экране.

Если граница всё ещё не ясна, поможет внешний ревьювер. Oleg Sotnikov на oleg.is работает со стартапами над архитектурой продукта и Fractional CTO‑консалтингом, включая такую очистку, когда общие компоненты начинают нести продуктовые правила, которым им не место.

Сделайте аудит, напишите правило и почистите один компонент на этой неделе. Один хороший пример делает остальную кодовую базу намного проще для оценки.

Часто задаваемые вопросы

What is prop soup?

Prop soup — это общий компонент, который собирает слишком много пропсов, флагов и одноразовых настроек. Он начинается простым, а затем разрастается до состояния, когда люди тратят больше времени на расшифровку API, чем на использование компонента.

How can I tell a shared component has gone too far?

Хороший признак — путаница. Если люди спрашивают, какую комбинацию пропсов можно безопасно использовать, или нужны тесты, чтобы предсказать результат, граница нарушена и компонент, вероятно, содержит правила, которым не место в общей библиотеке.

Which props belong in a shared component?

Оставляйте в общей библиотеке визуальные и состояния UI. Такие вещи, как размер, тон, отступы, disabled, loading, error и слоты для содержимого обычно подходят, потому что они описывают, что UI должен показывать.

What should stay out of a design system component?

Оставляйте продуктовые решения в коде фич. Проверки планов, роли, правила одобрения, логика оформления заказа и региональные правила должны находиться рядом со страницей или фичей, которая ими управляет, а затем передавать обычные UI-пропсы вниз.

When should I create a wrapper instead of adding a prop?

Используйте обёртку, когда одна продуктовая область требует особого поведения, но базовый компонент по‑прежнему подходит для отображения. Обёртка может решить текст, состояние disabled, иконки и обработчик клика, не нагружая базовый компонент знанием о биллинге, правах доступа или workflow.

How do I audit a messy component?

Начните с перечисления всех пропсов и разделения их по назначению. Обычно видно визуальные пропсы, интерактивные, расплывчатые имена, продуктовые правила и пропсы, конфликтующие друг с другом. Это сильно упрощает путь очистки.

Should I rewrite a messy component from scratch?

Большинству команд лучше делать постепенный рефакторинг, а не переписывать с нуля. Заморозьте добавление новых пропсов на короткое время, добавьте тонкую обёртку для старых экранов и выносите продуктовые правила по одному, чтобы старые страницы продолжали работать.

What do I do about props that conflict with each other?

Если два пропса могут конфликтовать, исправляйте API вместо того, чтобы заставлять каждого вызывающего делать догадки. Разделите компонент, переименуйте пропсы или переместите логику в обёртку, чтобы у каждого места вызова был один очевидный путь.

Should shared components know about billing, roles, or API data?

Нет. Общий компонент должен фокусироваться на рендеринге и взаимодействии, а не на политике продукта или загрузке данных из API. Контейнер, хук страницы или обёртка должны сначала загрузить данные и принять решения.

How do we stop prop soup from coming back?

Установите простое командное правило и следите за ним в код‑ревью. Если новый проп нужен только для одного экрана, звучит временно или описывает политику продукта, держите его вне общей библиотеки и решайте в коде фичи.