Claude Code в React-проектах: как ускорить сборку UI и при этом держать компоненты чистыми. Хуки, дизайн-система, типизация, чеклист.

Ускорение UI с помощью ИИ часто понимают как "сделать экран за вечер". Но скорость бывает разной: кому-то нужен быстрый прототип для согласования, кому-то - регулярная поставка фич без поломок, а кому-то важнее сократить число правок от дизайнеров и продакта. Если цель не назвать вслух, команда начинает гнаться за любым приростом скорости, а цена приходит позже.
В React-проектах типичная история такая: первые 30% идут очень быстро. ИИ хорошо накидывает разметку, состояния, заглушки данных, кнопки, модалки. А дальше начинается самое дорогое: привести компоненты к единому стилю, подружить их с дизайн-системой, аккуратно разложить логику, типизировать и не размазать ответственность по файлам.
Почему так происходит? ИИ чаще оптимизирует "видимый результат" (экран появился), но не "стоимость изменений". Через неделю нужно поменять валидацию или добавить новый статус, и внезапно оказывается, что логика спрятана в нескольких компонентах, названия разъехались, а похожие элементы UI сделаны тремя разными способами.
Признаки, что проект уже катится в "компонентный зоопарк":
isLoading2, variantAlt, data2, а типы либо any, либо слишком общие;Простой пример: ИИ сгенерировал карточку товара, а потом понадобились версии "компактная" и "расширенная". Вместо одного базового компонента с вариантами появляются две копии, затем третья "для мобильного", и вскоре правки нужно делать в трех местах.
Здесь цель простая: сохранить скорость, но вернуть контроль. Компоненты должны быть тонкими, поведение - жить в кастомных хуках React, UI - опираться на дизайн-систему, а TypeScript - страховать от случайных поломок. Тогда ускорение работает не только на старте, но и на последующих изменениях.
Когда интерфейс пишется быстрее за счет подсказок и генерации кода, главный риск не в ошибках верстки. Риск в том, что каждый новый экран приносит в компонент еще кусок правил, запросов, таймеров и условий. Через месяц такой компонент страшно трогать, а переиспользовать его невозможно.
Первое правило: компонент отвечает за отображение. Он принимает подготовленные данные и колбэки, показывает состояния (loading, error, empty) и отдает события наверх. Бизнес-правила (кто что может, какие поля обязательны, как собирается payload) не должны жить внутри JSX.
Логику и побочные эффекты лучше выносить в кастомные хуки. В хуке удобно держать загрузку и сохранение данных, локальные состояния формы и валидацию, преобразования данных для UI (маппинг, сортировки, вычисляемые поля), а также интеграции вроде аналитики, уведомлений и синхронизации с URL.
Так получается тонкий компонент, который легко читать, и хук, который легко тестировать и переносить. Это особенно полезно, когда вы часто просите ИИ дописать фичу: границу ответственности можно фиксировать прямо в запросе - "все правила в хук, компонент только рендерит".
Второе правило: UI строится вокруг дизайн-системы, а не вокруг разовых экранов. Если кнопка, инпут и модалка берутся из единого набора, у вас меньше копий и меньше случайных вариантов. Тогда хук управляет состояниями, а компонент собирает экран из готовых кирпичиков.
Третье правило: один источник правды для типов и моделей данных. Опишите сущности и DTO рядом с доменом (например, User, Order, FormState) и используйте их в хуках и компонентах.
Мини-пример: вместо компонента на 300 строк с обработчиками создайте useProfileForm(), который возвращает values, errors, isSaving и onSubmit. Компонент ProfileForm просто раскладывает это по полям из дизайн-системы и показывает сообщения об ошибках.
Чаще всего "раздуваются" компоненты: в них копится стейт, эффекты, обработчики, условия для разных режимов. Самый быстрый способ вернуть порядок - вынести поведение в кастомный хук и оставить в компоненте только UI.
Начните с "болезненного" компонента. Хорошие кандидаты: много useState, несколько useEffect, ветвления по ролям или статусам, сложные обработчики onClick/onSubmit, локальные костыли для загрузки, ошибок и уведомлений.
Старайтесь переносить логику так, чтобы внешний результат не изменился. Тогда правки легче проверить.
userId, начальные значения, колбэки), и что он возвращает (данные, флаги, обработчики).handleSubmit, handleChange, onRetry) и возвращайте их из хука.Пример: был UserSettingsForm на 250 строк, где одновременно грузятся данные, маппятся ошибки валидации, считается isDirty, и есть режимы (просмотр или редактирование). После выноса появляется useUserSettingsForm({ userId, onSaved }), который возвращает values, errors, status, isSaving, submit, reset. Компонент начинает просто рисовать поля и кнопки.
Короткие комментарии рядом с type/interface или над use... экономят время, особенно когда код быстро генерируется и часто меняется.
idle/loading/success/error).Перенос не делает код "умнее", но делает его устойчивее: UI живет отдельно, а поведение можно повторно использовать и тестировать без лишней разметки.
Когда UI делается быстрее, чаще всего ломается не логика, а единый вид. Появляются "почти такие же" кнопки, поля и отступы, которые живут отдельно и расходятся уже через неделю.
Дизайн-система - это не только набор компонентов. Это договоренность о базовых кирпичиках интерфейса: кнопки и поля, типографика, цвета, отступы и сетка, состояния (disabled, loading, error), а также правила сочетания. Если это не зафиксировано, команда неизбежно начинает "рисовать заново".
Работает простое правило: новый UI сначала собираем из существующего, и только если не получается - расширяем дизайн-систему. Чтобы это не оставалось пожеланием, помогают ревью-стопперы:
Держите базовые примитивы в одном месте и используйте их как единственный вход в стили. Даже если код генерируется, ревью должно отвечать на простой вопрос: "не появился ли второй набор кнопок".
Новый компонент дизайн-системы нужен, если паттерн повторится минимум в 2-3 местах и у него есть понятные варианты и состояния (например, Input с ошибкой, подсказкой и префиксом). Если это единичный экран, чаще достаточно композиции: Layout + Typography + Button.
Варианты лучше выражать пропсами, а не копипастой. Например, вместо PrimaryButton и DangerButton - один Button с variant.
type ButtonVariant = "primary" | "danger" | "ghost";
type ButtonProps = {
variant?: ButtonVariant;
size?: "sm" | "md";
loading?: boolean;
};
export function Button({ variant = "primary", size = "md", loading }: ButtonProps) {
// классы и токены зависят только от variant/size/state
return <button disabled={loading}>{loading ? "..." : "OK"}</button>;
}
Чтобы решения не терялись, фиксируйте их прямо в коде короткими примерами использования рядом с компонентом: 2-3 кейса, какие variant и состояния считаются нормой. Это быстрее, чем отдельные документы, и помогает держать UI единым при высокой скорости изменений.
TypeScript - хороший "тормоз" для хаоса. Он быстро показывает, где ИИ сгенерировал несовместимые данные, сломал контракт хука или сделал компонент слишком "универсальным".
Максимальный эффект дают типы на границах: в публичном API компонентов и хуков. Если компонент принимает 3-5 понятных пропсов с точными типами, его сложно превратить в комбайн. То же с хуками: лучше вернуть конкретный объект (данные, флаги, обработчики), чем "что-то" с непонятными полями.
Заранее договоритесь, где живут типы: доменные модели, DTO от бэкенда, форма и ее ошибки - это разные вещи. DTO может содержать лишние поля, а форма часто хранит строки и черновики.
Отдельно стоит типизировать статусы загрузки и ошибок. Это снимает половину лишних проверок и делает UI предсказуемым.
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
С таким union-типом код сам подсказывает, что в состоянии success данные точно есть, а в error вы не забудете показать текст.
Контроль чаще всего теряется из-за мелких уступок: any (особенно в данных формы и ответах API), слишком общий тип вроде Record<string, any> вместо явной структуры, "магия" дженериков, которую потом никто не понимает, и возврат из хука "мешка" с полями без четких типов.
Если просите ИИ сгенерировать типы, задавайте рамки: отделить DTO от модели, описать union-статусы, добавить 1-2 примера использования и избегать условных типов без явной причины. Так типизация помогает команде, а не превращается в ребус.
Когда код появляется быстро, больше всего страдает предсказуемость. Через пару недель никто не помнит, где лежит нужный хук, почему два похожих компонента называются по-разному и откуда тянется "магическая" константа.
Хороший стартовый каркас можно держать простым: папки должны отражать тип кода и его уровень переиспользования.
Обычно хватает разделения: ui для примитивов интерфейса, components для сборных блоков, hooks для логики, features для законченных частей продукта, pages для маршрутов или экранов, shared для общих вещей.
Чтобы это работало, держите минимальные правила импорта:
shared можно импортировать откуда угодно, но он не должен импортировать features и pages.features может тянуть shared и ui, но не должен ходить в соседние features напрямую.pages собирает экран из features и не хранит "умную" логику, кроме композиции.Так вы снижаете риск циклических зависимостей и ситуацию "поменял утилиту, сломал полприложения".
Имена должны помогать поиску. Для хуков почти всегда работает шаблон use<Действие><Сущность>: useFormStatus, useUserFilters. Для компонентов - <Сущность><Роль>: OrderStatusBadge, UserFiltersPanel. Если компонент привязан к фиче, имя может повторять домен: Payments/PaymentMethodPicker.
Принцип "одна ответственность на файл" держит порядок: один компонент, один хук, одна утилита. Нарушать его можно, когда куски слишком мелкие и всегда используются вместе (например, types.ts рядом с хуком и компонентом фичи).
Утилиты и константы складывайте по смыслу. Если значение относится к конкретной фиче (статусы формы, локальные тексты, ключи кэша), храните рядом с фичей. В shared отправляйте только то, что реально используется в разных местах без знания домена.
Пример: вы сделали форму в features/profile. Внутри держите useProfileForm.ts, ProfileForm.tsx, profileForm.constants.ts. А общий formatPhone() уходит в shared/lib/formatPhone.ts, потому что его могут использовать и регистрация, и профиль, и поддержка.
Когда UI делается быстрее, чаще всего ломаются мелочи: забытый loading, неверный тип, лишний ререндер, состояние ошибки, которое никто не видел. Поэтому нужен короткий, но обязательный набор проверок перед PR.
Минимальный прогон должен быть быстрым, иначе его начнут пропускать. Обычно хватает пяти пунктов: форматирование, линтер, проверка типов TypeScript, unit-тесты (хотя бы для критичной логики) и сборка или preview, чтобы убедиться, что проект вообще собирается.
Отдельно закладывайте 5 минут на визуальную проверку. Ревьюер не должен угадывать, как выглядит компонент в нестандартных состояниях. Проверьте руками (или в Storybook, если он есть) загрузку (блокировка кнопок и защита от двойного сабмита), ошибку (понятный текст и возможность повторить), пустые данные (не белый экран) и базовую доступность (фокус с клавиатуры, читаемые лейблы).
Снэпшоты полезны, когда UI реально стабилен: для маленьких "атомов" дизайн-системы (кнопка, бейдж, иконка) или для очень простых представлений. Но для форм и динамических страниц они часто создают шум. Там лучше точечные проверки: что при ошибке показывается сообщение, что кнопка disabled, что список рендерит N элементов.
Чтобы быстро ловить регрессии в логике кастомного хука, не нужна сложная инфраструктура. Главное, чтобы хук был "чистым": входы через параметры, выходы через понятный объект. Тогда тесты получаются короткими: успешный сценарий, ошибка и блокировка повторной отправки.
И зафиксируйте Definition of Done для UI-фичи. Например: все состояния (loading/error/empty) есть, типы не any, повторяемый UI взят из дизайн-системы, логика вынесена в хук, есть минимум один тест на ключевую ветку.
Когда подключаете генерацию кода, скорость растет, но вместе с ней легко растет и беспорядок. ИИ охотно делает "еще один компонент" вместо того, чтобы остановиться и обобщить решение.
Частый сценарий: появляются UserCard, UserCardCompact и UserCardNew, которые отличаются двумя пропсами и отступами. Через неделю правка в дизайне требует изменить три места, и вы уже не уверены, какой вариант используется на главном экране.
Быстрые сигналы, что вы идете не туда: похожие компоненты с разными именами, JSX превращается в простыню из условий и вычислений, состояние поднимают "на всякий случай", стили задают вручную мимо дизайн-системы, а TypeScript допускает невозможные состояния.
Держать рост под контролем помогают три привычки: отделять "что показываем" от "как работает" (логика в хуке), обобщать один смысл в один компонент (вариации через пропсы), и держаться дизайн-системы для базовых элементов и отступов.
Представьте форму регистрации: email, пароль, подтверждение пароля. Есть клиентская валидация, сервер может вернуть ошибку (например, "email уже занят"), а кнопка должна корректно уходить в disabled на время отправки. Если ускорять такой UI генерацией, легко получить огромный компонент, где вперемешку JSX, правила валидации, сетевые запросы и статусы.
Рабочая разбивка простая: UI-компоненты остаются тонкими, вся логика уезжает в хук, а запросы прячутся за маленьким адаптером API. Так вы можете менять дизайн, не ломая логику, и наоборот.
Явно смоделируйте состояния, чтобы не хранить пять разрозненных флагов. Минимальный набор обычно такой: idle (ничего не делаем), submitting (отправляем), success (успех), error (ошибка). Это упрощает disabled-состояния, тексты кнопок и показ сообщений.
type FormStatus = 'idle' | 'submitting' | 'success' | 'error'
type RegisterValues = {
email: string
password: string
passwordConfirm: string
}
type RegisterError = {
field?: keyof RegisterValues
message: string
}
export function useRegisterForm(api: { register(v: RegisterValues): Promise<void> }) {
// values, errors, status, submit(), setField(), validate()
}
Дизайн-система подключается без боли, если форма собирается из стандартных блоков: Field, Input, HelperText, Alert, Button, FormLayout. Компонент формы просто раскладывает их и передает props из хука. Важно, что сообщения об ошибках и состояния disabled берутся из одного источника правды.
Если просите ИИ помочь, полезно просить не "собрать весь компонент целиком", а подготовить каркас хука и типы: структуру useRegisterForm, переходы статусов, что вернуть наружу (values, errors, status, handlers) и пару примеров использования. UI-слой лучше оставить под вашу дизайн-систему.
Чтобы ускорение UI не превратилось в хаос, полезно прогонять один и тот же набор вопросов перед каждым слиянием.
Отдельно посмотрите на дизайн-систему. Если в проекте уже есть готовые компоненты и токены, ручные стили почти всегда сигнал, что вы создаете еще одну версию кнопки или поля.
any, а сложные места типизированы через union-типы.if.Следующий шаг для команды - закрепить правила в шаблоне фичи: структура папки (component + hook + types + тесты), именование (useXxx для хуков) и явные границы ответственности (что можно в компоненте, а что только в хуке).
Если вы собираете прототипы и приложения из чата и хотите сохранять контроль над кодовой базой, это удобно делать в TakProsto: можно быстро получить основу, а затем работать с экспортированными исходниками по тем же правилам архитектуры, дизайн-системы и типизации. Платформа доступна на takprosto.ai.
Если вы делаете только прототип для согласования, можно жертвовать частью архитектуры. Если цель — регулярно добавлять фичи без поломок, тогда важнее снижать стоимость изменений: тонкие компоненты, логика в хуках, единые UI-примитивы, четкие типы.
Перед стартом зафиксируйте цель в одном предложении и держите ее в ревью: что оптимизируем — скорость первого экрана или скорость изменений через месяц.
Первые экраны ИИ собирает быстро: разметка, состояния, заглушки, базовые элементы. Хаос начинается, когда нужно:
ИИ часто оптимизирует «экран появился», а не «экран легко менять».
Короткий чек на признаки:
any, странные суффиксы, дубли);Если видите 2–3 пункта — пора «возвращать контроль».
Держите правило: компонент рендерит, хук управляет.
Компонент:
loading/error/empty;Хук:
values/errors/status/handlers.Это проще поддерживать и просить ИИ дописывать частями, не ломая структуру.
Самый безопасный путь — перенос «без изменения поведения»:
useState/useEffect и вычисления.submit, retry, changeField) и возвращайте их наружу.В конце компонент должен читаться быстро: минимум условий, максимум сборки UI из примитивов.
Рабочее правило: сначала используем существующее, потом расширяем систему.
Практические «стопперы» в ревью:
Так вы не создаете параллельные наборы UI и не размножаете поддержку.
Добавляйте компонент дизайн-системы, когда паттерн:
error, disabled, loading);Если это единичный экран — чаще достаточно композиции существующих примитивов (layout + типографика + кнопки) без нового «уникального» компонента.
Ставьте типы на границах:
Полезный минимум:
idle/loading/success/error), чтобы UI не жил на россыпи флагов;Так TypeScript ловит несовместимости сразу, а не после серии правок.
Чаще всего контроль теряется из-за:
any в ответах API и данных формы;Record<string, any> вместо структуры);data, но статус loading).Быстрый фикс: запретить any на публичных границах и ввести единый тип статуса/ошибки для фичи.
Минимальный, но обязательный набор:
loading, error, empty, disabled, фокус с клавиатуры.Цель — ловить мелкие регрессии быстро, иначе скорость разработки «съедается» правками.