Один із моїх колег у Spatie — славнозвісний Фрік (Freek), потужний back-end інженер (і це не спроба йому підлеститися) з меншим досвідом у front-end (вибач, Фрік), — вирішив перевірити, чи здатний AI повністю написати front-end для реального продукту. Сміливий експеримент чи безглузда авантюра? Неважливо! «There There» — це платформа клієнтської підтримки з тікетами, каналами, AI-агентами, віджетом чату для сайтів та багатьма іншими функціями.
«Чи міг би ти поглянути на front-end і оцінити його? Чи придатний цей код для production?» — запитав він.
Протягом двох днів я займався саме цим. Якщо коротко: код на диво якісний локально, але стає дедалі суперечливішим глобально. Це цікавий результат, адже сам продукт працює чудово. Ми вже використовуємо його всередині компанії. Тож я хочу розповісти, як проводив цей аудит коду та як мені допомагав Claude.
AI не вміє робити рев'ю
Перш ніж відкрити бодай один файл, я вирішив: я міг би просто попросити AI проаналізувати кодову базу, опублікувати результат і два дні спокійно дивитися Netflix. Звісно, я цього не зробив. Такий підхід дав би правдоподібний відгук, але не справжній. До того ж мене б мучило сумління через Netflix...
Справжнє рев'ю визначає: «це реальний баг», «це питання смаку», «це прийнятно», а «це — категорично ні». AI чудово складає списки, але він не здатний мати власну позицію.
Не починайте з AI. Спочатку я хотів вибудувати власну ментальну модель: зрозуміти домени, межі та структуру папок, почитати код, простежити логіку. Без цього базису я б просто машинально погоджувався з кожною відповіддю чат-бота.
Використовуйте AI механічно. Як grep на стероїдах: порахувати, перелічити, знайти те, що я можу перевірити поглядом. Використовуйте його як генератор кандидатів: нехай підсвічує все, що суперечить моїм стандартам, а я вже вирішу, що насправді є проблемою.
Перші кілька годин я провів наодинці з деревом папок There There. Без AI. Намагався зрозуміти, чим є домени «tickets», «channels», «brains», «recaps», як вони пов'язані та де проходять їхні межі. Я питав себе: «Де б я розмістив цей код, якби писав його сам?» Мені потрібна була власна система координат.
Потім настав час AI. Я використав його як лічильник і генератор. Для генерації я написав розлогий промпт із проханням знайти баги, React anti-patterns, проблеми з продуктивністю та безпекою, невідповідності. AI видав каталог на 600 рядків із тегами серйозності та номерами рядків. Це було вражаюче — значно більше, ніж я міг би знайти вручну.

Далі почалася справжня робота: валідація кожного пункту. Чесно кажучи, це було нудно, але допомогло ще краще зрозуміти код. Дещо виявилося реальними багами, дещо — свідомими компромісами, а дещо — просто стилістичними уподобаннями, замаскованими під критичні помилки. Ця валідація і є справжнім рев'ю. Усе інше — лише списки. Спойлер: я так і не дочитав список до кінця — обсяг коду та перевірок для двох днів виявився завеликим. Через заплутаність коду між доменами я не встиг знайти рішення для кожної проблеми. Але висновок уже сформувався: глобально в коді панує хаос.
Етап 1: Архітектура
Спершу я поглянув на загальну картину: структуру папок, напрямки імпортів і розташування доменів.
Структура складається з кількох основних папок: common/, modules/, pages/, а також окремої widget/ для чату. UI-примітиви взяті з shadcn/ui. Спільні високорівневі елементи лежать у common/. Код окремих функцій — у modules/tickets/, modules/chat-agent/ тощо. Сторінки (pages/) зшивають усе докупи. Це логічна структура, з якої я б і сам почав роботу. AI не допомагав мені її зрозуміти, але я використав його, щоб перевірити, чи не пропустив я чогось очевидного.
Але відкривши файли в common/, я зупинився. AiSetupPrompt.tsx використовувався лише в налаштуваннях робочого простору. ChannelBadgeList.tsx — так само. Компоненти редактора AI-чату в common/components/ai-chat/ використовувалися тільки в modules/agent-chat/. Це не common. Це специфічні компоненти функцій, які опинилися в common/ лише тому, що ніхто не заборонив AI їх туди класти.
Тут AI виправдав себе. «Для кожного файлу в common/components склади список файлів, які його імпортують». За кілька хвилин я отримав результат, і закономірність стала очевидною: купа файлів мала б належати конкретним модулям. Я впізнав цю ситуацію — «тимчасове» в common/ стає постійним швидше, ніж ви встигнете це помітити.
Для кожного файлу в common/ перелічи всі файли поза межами common/, які його імпортують. Формат: common/path/File.tsx → [список імпортерів]. Відсортуй за кількістю імпортерів (за зростанням). Познач файли з 0 або 1 імпортером.
Перевір напрямок імпорту. Знайди випадки, де:
- common/ імпортує з modules/ або pages/
- modules/ імпортує з pages/
- modules/X імпортує з modules/Y (вкажи обидва напрямки)
- widget/ імпортує з будь-якого місця поза widget/ та common/
Перелічи директорії в pages/ з понад 20 файлами. Для кожної вкажи кількість файлів і коротко припусти, чи є вміст: (1) простою оркестрацією сторінки, (2) доменною логікою для modules/, або (3) змішаним.
З іншого боку все було ще гірше. Папка pages/settings/workspace/ містила 75 файлів. Це був не інтерфейс налаштувань, а справжня доменна логіка: майстри налаштування каналів із верифікацією DNS, редактори воркфлоу, білінг зі Stripe, керування AI-мозками через MCP. Цілий продукт, який так і не винесли в модулі, бо робота почалася в папці сторінок, і там все й залишилося.
Щоправда, самі напрямки імпортів були чистими: common не звертався до modules, а widget був повністю ізольований. Це добре. Але це була лише випадкова конвенція, ніде не зафіксована. Наступна сесія з AI, який не знає правил, легко б їх порушила. Такі правила мають бути прописані — це б допомогло уникнути глобального хаосу.
Етап 2: Компоненти та хуки
Далі я перейшов до деталей. Відкрив pages/tickets/Index.tsx і прочитав його майже повністю. Понад тисячу рядків. Це не сторінка, а цілий застосунок всередині файлу. Я попросив AI надати цифри, адже він чудово знаходить відхилення від стандартів.
Для pages/tickets/Index.tsx надай:
- кількість useState
- кількість useRef
- кількість useEffect (і для кожного: масив залежностей та короткий опис, чи це реальний side effect, чи обчислюваний стан, який мав би бути useMemo)
- кількість useCallback / useMemo
- загальну кількість props для найбільших компонентів у файлі
- кількість полів у об'єктах context value
Просто дай цифри. Я сам вирішу, що з цим не так.
Порахуй props для: Composer, CommandPalette, AgentChatContext. Вкажи загальну кількість, скільки з них є onX колбеками, скільки boolean, скільки доменними об'єктами.
Результати: 29 викликів useState/useRef в одному файлі. 14 пар useCallback/useEffect. Composer з 17 пропсами. AgentChatContext із 22 полями в контексті. Ці цифри перетворили моє розмите «тут забагато всього» на чіткий сигнал. Це «божественні компоненти», інтерфейс яких розширюється з кожною новою функцією. «Смерть від тисячі колбеків». AI допоміг мені базувати висновки на фактах, а не на відчуттях.
Ось кілька багів, які виявив AI (я все ще мав їх перевірити, але швидкість пошуку вразила):
useDraftManagementзатирає введення користувача. Ефект відновлення залежить від[ticketUlid, mode, replyTemplate].replyTemplate— це звичайний об'єкт-пропс. Будь-який ре-рендер створює новий об'єкт, перезапускає ефект і затирає те, що пише користувач. Реальний баг.useChannelреєструє застарілі колбеки. Обробники зберігаються в ref, але підписка Echo використовує лише той колбек, який отримала першим. Подальші оновлення ref ігноруються. Реальний баг.- Мемоізація в
AgentChatHistoryніколи не оновлюється.useMemoзалежить відcurrentTicketRef. Оскільки цеMutableRefObject, його ідентичність стабільна. Memo назавжди «прив'язаний» до першого рендеру. Користувач бачить повідомлення не того тікета. Критичний баг. - Стилістичний хаос із
forwardRef. У двох файлах використовується застарілийforwardRef, в усіх інших —refяк звичайний пропс (як того вимагає React 19). Це міна сповільненої дії для майбутніх правок AI.
Проте в одному випадку я не погодився з AI. Він позначив useState<string | undefined>(undefined) як помилку, бо всюди в проєкті використовується null. Технічно це невідповідність, але один випадок проти тридцяти — це просто виняток, а не критична проблема. AI не відчуває контексту, він бачить лише факт невідповідності.
Локально — добре, глобально — хаос
Після двох етапів перевірки вималювалася чітка картина.
Окремі файли в цій кодовій базі чудові. Скрізь Tailwind через cn(), жодної інтерполяції рядків у класах. Темна тема через семантичні токени (text-muted-foreground, bg-card). Функціональні компоненти, однаковий іменування, shadcn/ui як база. Якщо відкрити випадковий файл, ви, скоріш за все, кивнете на знак згоди. AI справді добре пише окремі файли.
Проблема починається там, де файли взаємодіють.
Три різні способи рендеру порожнього стану (empty state). Два дублікати formatRelativeTime з різною логікою. Токен text-destructive в одному місці та захардкоджений text-red-500 в іншому. Помилки форм через <FormField> у 12 файлах і копіпаст <p> у 15 інших. Чому AI не використав готові типи для статусів тікетів? Бо йому ніхто не сказав цього робити.
Це не помилки AI як такі. Кожне окреме рішення виглядає логічним. Проблема в тому, що між сесіями немає спільної пам'яті. Кожне рішення приймається з нуля. Локальна послідовність не означає глобальну.
Звід правил (Ruleset)
Результатом мого аудиту став короткий список правил, які варто додати в README.md. Це не просто стайл-гайд, а набір правил, які може контролювати лінтер:
Три шари:
common/: для коду, що використовується у понад двох модулях і не має доменної логіки.modules/X/: доменно-залежний код (компоненти, хуки, типи для функції X).pages/X/: лише оркестрація та специфічна верстка сторінки.
Напрямок імпорту: common ← modules ← pages. common нічого не імпортує. modules — тільки з common. pages — з обох. Ми можемо контролювати це через ESLint no-restricted-imports.
Один хук/компонент на файл. Винятки не варті того хаосу, який вони створюють.
Заборона forwardRef. У React 19 використовуйте ref як звичайний пропс.
Керування станом за ієрархією:
- Дані сервера — через Inertia props.
- Спільні дані —
useSharedData. - Стан в URL (фільтри, пошук) — через параметри URL.
- Локальний UI (діалоги, інпути) —
useState. - Крос-компонентний стан — через контекст фічі.
Принцип: стан має бути якомога далі від DOM. Пріоритет: URL > shared > context > local.
Помилки форм — тільки через <FormField>. Жодних ручних <p>.
Список короткий свідомо. Якщо дати AI забагато правил, він почне помилятися. Але з цим набором більшість знайдених мною проблем була б вирішена ще на етапі написання коду.
Висновки
Коли я почув, що front-end повністю згенерований AI, я очікував на катастрофу. Натомість я знайшов кодову базу, яка локально виглядає краще, ніж я очікував. Компоненти сучасні, функціональні та охайні.
Проте без «якоря» код починає дрейфувати. Кожен файл пишеться так, як AI «здалося правильним» саме в цю секунду. Тисяча таких файлів — і ви отримуєте хаос. Розробник з часом виробляє відчуття того, що є «правильним» для конкретного проєкту. AI цього не має. Кожна сесія для нього — це перший робочий день.
Мій висновок не в тому, щоб відмовитися від AI. А в тому, що AI — це інструмент. Якщо ви використовуєте його для front-end, спочатку напишіть правила, зафіксуйте їх у репозиторії та згодовуйте AI у кожній сесії. Не віддавайте йому кермо повністю.
Без AI це рев'ю тривало б значно довше. Він допоміг мені з важкою та нудною роботою, посиливши мій прогрес, а не замінивши мене. Аудит перетворився на звід правил, і саме ці правила (сподіваюся) врятують наступні сотні файлів від перетворення на чорну діру невідповідностей.