Головна / Статті

Вибір структури папок у React: сім схем та їхні межі ефективності

Порівняйте плоскі, типові, орієнтовані на функції, Atomic Design, DDD, Feature-Sliced Design та монорепозиторійні схеми для React та дізнайтеся, на якому рівні ефективності кожен з них припиняє працювати.

Видавець: REACTAPP.TOP31 хв читання7020 слів

Джерела: React Folder Structure: 7 Ways to Organize a React App (And Exactly When Each One Breaks)

Кожна розвиваючася база коду React рано чи пізно стикається з одним і тим самим запитанням: куди слід покласти цей файл? Офіційної відповіді немає, проте команди витрачають цілі спринти на міграцію між різними структурами, а співрозмовники під час співбесід все ще запитують кандидатів про це. У цьому посібнику порівнюються сім поширених способів організації React-додатку — від простої папки src/ до монорепозиторію з кількома додатками, при цьому використовується один зразковий додаток. Для кожної структури ви побачите, що це таке, хто насправді її використовує, у чому її сильні сторони, де є проблеми та точка, у якій вона перестає функціонувати, щоб ви могли обрати найбільш підходящу структуру для вашого додатку.

Ніхто офіційний не вибере її за вас

У документації React версії Legacy колись була сторінка про структуру файлів. У ній спочатку зазначалося, що React не має думки щодо того, як розміщувати файли у папках, і радилося всім, хто починає проект, не витрачати більше п’яти хвилин на вибір структури. Цю сторінку згодом видалили, коли документація перейшла на react.dev, і її нічим не замінили. Сьогодні React фактично нічого не каже про папки. Старі рекомендації збереглися на архівній сторінці FAQ.

Фреймворки та інструменти, пов’язані з React, також не мають чітких рекомендацій. Next.js описує себе як не маючий думки щодо того, як організовувати та розміщувати файли проекту. Шаблон React від Vite генерує кілька вихідних файлів без жодних папок. Жодна з організацій у екосистемі не вказує, що робити.

Ця відсутність багато чого пояснює. Такі структури, як Bulletproof React, накопичили десятки тисяч зірок на GitHub переважно завдяки пропозиції певної структури папок, і розробники постійно сперечаються з цього приводу. Чесне формулювання всього, що буде далі, полягає у тому, що ці сім структур є конвенціями, а не стандартами. Жодна з них не схвалюється самим React. Важливо зрозуміти витрати кожної з них та, насамперед, те, де вони призводять до проблем, адже саме це стає вирішальним, коли застосунок стає реальним.

Два принципи, які лежать в основі кожної структури

Творці більш спокійні, ніж їхні прихильники

Люди, які створили ці методології, зазвичай сприймають їх значно вільніше, ніж ті, хто їх просуває. Бред Фрост, творець Atomic Design, у 2019 році опублікував книгу „Extending Atomic Design“. У ній він описує, як до нього звертаються з проханням „виправити“ його термінологію, наголошує, що атомний дизайн — це не жорстке догма, та каже, що команди, які хочуть дев’ять етапів, названих на честь учасників гурту Chicago, можуть це робити. Ви можете прочитати її за посиланням Extending Atomic Design. Творець методу каже, що назви не мають значення, тоді як десь процес перегляду коду застряг через питання, чи вважати певний компонент молекулою.

Старі документації React також згадували Atomic Design за назвою. У них зазначалося, що деякі розробники розділяють компоненти на папки відповідно до їхньої ролі в додатку, наводили Atomic Design як приклад такого підходу та рекомендували сприймати такі методології як корисні приклади, а не суворі правила. Оскільки це походить з власної документації React, це найкраща критика суворого застосування Atomic Design у React, яка існує.

Screaming Architecture

У цій дискусії насправді існує лише один вагомий аргумент, і він є старшим за React. У вересні 2011 року Роберт К. Мартін опублікував Screaming Architecture. Його запитання: коли хтось дивиться на верхній рівень каталогів вашого додатку, чи бачить він одразу систему охорони здоров’я, бухгалтерську систему чи систему управління запасами? Його висновок полягав у тому, що архітектура має розповідати читачам про саму систему, а не про фреймворки, які вона випадково використовує.

Ця ідея лежить в основі кожної структури, заснованої на компонентах. Директорія src/, яка містить папки components/, hooks/, utils/ та services/, свідчить про те, що це додаток на React. Директорія src/, яка містить папки checkout/, billing/ та inventory/, розкриває суть діяльності компанії. Кожна структура, описана в цьому посібнику, є або варіацією цієї ідеї, або її свідомим відхиленням.

Колокація

Другий принцип є найважливішим у повсякденній роботі. Кент С. Доддс називає його колокацією: тримайте код якомога ближче до місця, де він є корисним. Він приписує Дену Абрамову більш точне формулювання — речі, які змінюються разом, повинні знаходитися якомога ближче одна до одної. Повний опис цього принципу доступний у статті Colocation.

Якщо дивитися на це з такої точки зору, кожна структура в цьому посібнику відповідає на одне запитання: які частини вашого коду розвиваються як єдине ціле? Якщо ви правильно відповісте на це запитання, папки майже самі собою сформуються. Якщо ж ви помилитеся, жодна методологія не зможе вас врятувати.

Примітка щодо популярного цитатування: порада «переміщувати файли, поки все не здасться правильним» часто приписується Дену Абрамову та іноді наводиться як офіційна документація React. Це не є частиною документації; у документації наведена порада на п’ять хвилин, а цитата Абрамова походить з твіту, який, схоже, було видалено. Ідея є слушною; сприймайте це цитування як народну легенду.

Одне додаток-джерело, сім варіантів організації

Порівнювати макети доцільно лише тоді, коли додаток залишається незмінним; інакше ви будете порівнювати приклади, а не структури. Тож кожен з наведених нижче макетів використовує той самий невеликий інтернет-магазин із тими самими файлами:

  • Функції: автентифікація (вхід та реєстрація), каталог (список товарів та їхні деталі), кошик, процес оплати та замовлення.
  • Примітиви інтерфейсу: Button, Input, Modal, Badge та Spinner.
  • Хуки: useAuth, useCart, useProducts, useCheckout та useDebounce.
  • Модулі API: authApi, productsApi, checkoutApi, ordersApi та спільний apiClient.
  • Інше: допоміжні функції cartStore та formatPrice.

Чотири з цих файлів є навмисно складними для розуміння, оскільки вони показують, де починаються проблеми кожного макету:

  • Modal.jsx отримує позначення «атом», «молекула» або «організм» залежно від особи, яка його класифікує, що підкреслює таксономію Atomic Design.
  • useDebounce.js є абсолютно універсальним, проте ним користується лише пошук у каталозі. Чи це глобальний хук чи хук каталогу?
  • CartTotal.jsx відображається як у вікні кошика, так і на екрані оформлення замовлення, тому невідомо, хто є його власником.
  • formatPrice.js схожий на просту утиліту, хіба що гроші є справжнім концептом у цій галузі, у такому разі він мав би бути об’єктом типу Money, а розміщення його у папці з утилітами є помилкою.

Пам’ятайте про ці чотири аспекти. Кожна структура вирішує їх по-різному, і саме ці розбіжності визначають відмінності між різними лейаутами.

Плоска структура

Що це таке

Усе знаходиться безпосередньо в src/, без або майже без папок, впорядковане алфавітно, оскільки ніщо інше не встановлює певного порядку.

Ніхто не оприлюднював це як методологію. Це просто те місце, де починається кожен проект, і де залишається чимало з них. Документація React навіть рекомендувала це як вихідну точку: якщо ви відчуваєте труднощі, тримайте всі файли в одній папці, а коли вона стане достатньо великою для розділення, ви зрозумієте, які файли зазвичай редагуєте разом. Це справді гарна порада. Структура, сформована на основі спостережуваних закономірностей змін, краща за ту, що нав’язується з самого початку.

Хто її використовує

Прототипи, тестові рішення, демонстрації в сандбоксі, проекти з хакатонів, домашні завдання та внутрішні інструменти з одним адміністратором. Крім того, у ньому описується чимало продакшн-додатків, які так і не були рефакторовані.

Офіційний шаблон Vite React по суті є простим. Виконання команди npm create vite@latest -- --template react-ts створює файли main.tsx, App.tsx, App.css, index.css та папку assets/, без наявності папок components/ чи features/. Оскільки цей шаблон не має жодних упереджень, дискусії щодо його використання переходять до кожної команди, яка ним користується.

Переваги

  • Навігація не коштує нічого. Існує лише одна папка, а інструмент вибору файлів знаходить їх за назвою. Оскільки не потрібно приймати рішень щодо розташування, не виникає й суперечок з цього приводу.
  • Рефакторинг є безкоштовним. Немає структури, яку можна було б порушити, тому немає чого мігрувати. Ви можете швидко діяти, з’ясувати, що насправді є в додатку, та організувати все після цього.
  • Він чесно ставиться до невизначеностей. У перший тиждень ви не знаєте меж своїх функцій, і проста структура не вдає, ніби ви їх знаєте. Будь-яка папка, створена на такому ранньому етапі, є припущенням, за яке вам може довестися платити, щоб його скасувати.
  • Слабкості

    • Він раптово припиняє ефективно працювати приблизно на 30 файлах. Провал відбувається не поступово: одного дня список файлів більше не поміщається у бічній панелі, і форма додатку зникає.
    • Алфавітний порядок не має сенсу. authApi.js знаходиться поруч із App.jsx лише тому, що обидва починаються на літеру A, тоді як useAuth.js та LoginForm.jsx», які ви завжди редагуєте разом, опиняються далеко одне від одного. Порядок сортування прямо суперечить логіці розташування елементів.
    • Немає жодних меж, які можна було б застосувати. Будь-який файл може імпортувати будь-який інший, циклічні залежності поступово з’являються непомітно, і до того часу, як ви захочете встановити межі, кожне порушення вже має значний вплив.
    • Видалення певної функціональності перетворюється на складну роботу зі з’ясуванням деталей. Щоб видалити процес оплати, потрібно прочитати всі файли, щоб з’ясувати, які з них до нього належать, адже не існує окремої папки для видалення.

    Де це призводить до проблем

    Приблизно при 30 файлах або у момент, коли до команди приєднується другий розробник, друга межа стає важливішою. Плоска структура працює тоді, коли одна людина має у своїй голові повну картину. З двома людьми з’являються дві картини, які негайно починають відрізнятися.

    Структура на основі типів

    Що це таке

    Файли групуються за їхнім призначенням: компоненти разом, хуки разом, виклики API разом, сторижні даних разом. У документації React для старих версій це описувалося як групування за типом файлу та представлялося як один із двох поширених підходів без вибору конкретного. Для демонстраційного додатку це означає Button, Modal, CartItem, CheckoutForm та всі інші компоненти у каталозі components/, усі хуки у hooks/, усі модулі API у api/ та cartStore у store/.

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

    Хто його використовує

    Майже кожен посібник з React, більшість проєктів у bootcampах та значна частина продакшн-додатків, які виросли за межі простого планування структури, використовують цю модель. Це найпростіший крок: як тільки папка src/ заповнюється, першою природною реакцією є створення папки components/.

    Переваги

    • Це не потребує пояснень. Нікому ніколи не доводилося пояснювати, що має знаходитися в папці hooks/, тож нові розробники можуть почати працювати вже з першого дня, використовуючи відому їм термінологію.
    • Класифікація є дуже простою. Компонент потрапляє до папки components/, хук — до hooks/, а сам файл вказує на його тип. Порівняйте це з Atomic Design, де навіть розсудливі люди можуть годинами сперечатися щодо одного компонента.
    • Ця модель ідеально підходить для невеликих проєктів. При кількості файлів менше 50 вона працює добре, а її заміна коштуватиме більше, ніж принесе користі.

    Слабкі сторони

    • Він оголошує про фреймворк, а не про продукт, що саме і заперечує підхід Screaming Architecture. Каталоги верхнього рівня з назвами components/, hooks/, api/ та utils/ повідомляють початківцю, що ви використовуєте React, але нічого не говорять про те, що робить додаток. Інтернет-магазин та система планування візитів у лікарні з кореневого рівня виглядають однаково.
  • Кожна зміна поширюється на всю структуру проекту. Додавання одного поля до кошика означає редагування файлів CartItem.jsx у директорії components/, useCart.js у директорії hooks/, cartApi.js у директорії api/ та cartStore.js у директорії store/. Це чотири директорії та чотири позиції прокрутки для однієї логічної зміни. Принцип розташування файлів порушується: файли, які найчастіше змінюються в одному коміті, розміщуються якомога далі один від одного.
  • components/ перетворюється на нову плоску директорію. У зразковому додатку вона містить близько 18 файлів без внутрішньої групування. Проблема плоскої структури вирішується для хуків та модулів API, а потім знову з’являється у директорії, яка містить більшу частину коду.
  • Видалення певної функції означає необхідність пошуку в кількох папках замість однієї. Щоб видалити процес оплати, потрібно переглянути чотири каталоги, сподіваючись, що нічого не пропущено.
  • Межі між елементами все ще відсутні. Ніщо не заважає ProductCard імпортувати useCheckout; папки мають виключно декоративне значення.
  • Де виникають проблеми

    Приблизно при 100 файлах або коли в components/ з’являється близько 20 елементів з’являється попереджувальний сигнал: розробники починають додавати до назв файлів префікси, такі як CartItem, CartDrawer та CartTotal, щоб імітувати групування, якого не забезпечують папки. Ці префікси — це прохання кодової бази створити спеціальні папки для кожної функції.

    Структура, заснована на функціях

    Що це таке

    Файли групуються за їхньою функцією. Кожна функція має свої компоненти, хуки, виклики API та стан у одному місці; спільні примітиви знаходяться у загальній області, а функції компонуються на рівні додатку без взаємного імпорту.

    У документації React минулих версій це групування називалося за функціями чи маршрутами, і пропонувався практичний спосіб визначення меж. Оскільки значення терміна „функція“ не є універсальним, ви самі обираєте рівень деталізації, а якщо ви не можете сформувати список верхньорівневих папок, ваші користувачі можуть описати основні частини продукту та використати свою уявну модель як креслення. Це, мабуть, найкорисніша практична порада щодо цієї теми: ваші користувачі вже знають, де знаходяться межі ваших функцій.

    Фактично це Screaming Architecture, перекладена на React, і сьогодні вона є основним вибором. У прикладному додатку features/cart/ містить компоненти кошика, useCart — API кошика та cartStore; features/checkout/ містить CheckoutForm, useCheckout та checkoutApi; і так далі, причому Button, Modal та інші примітиви знаходяться у спільній папці components/.

    Хто її використовує

    Найвідомішою референтною реалізацією є Bulletproof React від Алана Аліковича, яка мала близько 35,5 тис. зірок на GitHub та продовжувала активно підтримуватися на момент написання цього тексту. Це найпоширеніша структура React, яку копіюють, і документ з описом структури проекту Bulletproof React варто прочитати безпосередньо, замість того щоб покладатися на узагальнення з інших джерел.

    Рекомендована структура src/ складається саме з одинадцяти верхньорівневих папок, які наведені нижче разом із їхньою функцією. Зауважте, що лише одна з них, features, організована за доменами; решта — це спільні шари, якими користуються функції, а також шар app, який об’єднує все воєдино.

    src
    +-- app          # application layer (routes, app.tsx, provider.tsx, router.tsx)
    +-- assets       # static files such as images, fonts, etc.
    +-- components   # shared components used across the entire application
    +-- config       # global configurations, exported env variables etc.
    +-- features     # feature based modules
    +-- hooks        # shared hooks used across the entire application
    +-- lib          # reusable libraries preconfigured for the application
    +-- stores       # global state stores
    +-- testing      # test utilities and mocks
    +-- types        # shared types used across the application
    +-- utils        # shared utility functions
    

    Усередині кожної функціональності може бути до семи сегментів: api, assets, components, hooks, stores, types та utils. У документації чітко зазначено, що функціональність потребує лише тих сегментів, які вона фактично використовує.

    Два правила виконують основну роботу:

    • Без імпортів між функціональностями. Рекомендації React радять уникати ситуацій, коли одна функціональність імпортує іншу, та пропонують створювати функціональності на рівні застосунку, щоб кожна з них залишалася незалежною та кодбаза була простішою для розуміння.
    • Однонаправлений потік. Код має рухатися в одному напрямку — від спільного коду до функціональностей, а потім до застосунку (shared -> features -> app).

    Обидві правила можна застосовувати за допомогою правила ESLint import/no-restricted-paths. Саме цей крок контролю часто ігнорують багато команд, і саме тому з часом їхні межі починають розмиватися.

    Зміна підходу до файлів barrel: раніше Bulletproof React рекомендував використовувати файли barrel — index.js, який знову експортує публічні елементи функціоналу. Тепер же вони радять імпортувати файли безпосередньо, оскільки файли barrel можуть заважати процесу оптимізації в Vite та спричиняти проблеми з продуктивністю. Якщо ви опанували цей підхід зі старішого посібника, поради тепер змінилися. Оскільки саме файли barrel раніше використовувалися більшістю команд для опису публічного API функціоналу, відмова від них означає, що межі тепер визначаються виключно у налаштуваннях lint.

    Переваги

    • Зберігання файлів разом є стандартним. Одна зміна в кошику впливає лише на одну папку, адже файли, які змінюються разом, зберігаються в одному місці.
  • Видалення функції — це просто команда rm -rf. Видаліть папку features/checkout/, виправте імпорти, через які скаржиться компілятор, і все готово. Це має більше значення, ніж здається: код, який важко видалити, зазвичай ніколи не видаляється.
  • Домен видимий з кореня. Новий інженер, прочитавши auth/, catalog/, cart/, checkout/ та orders/, розуміє продукт ще до того, як відкриє хоча б один файл.
  • Межі можна застосовувати у вигляді помилок lint у CI замість коментарів до перегляду.
  • Ця структура добре масштабується — навіть понад 1 000 файлів, адже кожна нова функція є окремою папкою, яка не впливає на існуючі.
  • Слабкості

    • „Функція“ не має фіксованого визначення, тому будуть суперечки щодо неї. Файл CartTotal.jsx відображається як у кошику, так і під час оформлення замовлення. Жодна з варіантів не є правильною; потрібно просто прийняти рішення, яке, ймовірно, доведеться переглянути згодом.
    • Для спільного коду щоразу потрібно приймати рішення. Наразі useDebounce використовується лише для пошуку в каталозі. Якщо зробити його глобальним, папка спільних хуків заповниться елементами, які використовуються лише один раз. Краще залишити його у features/catalog/hooks/, а коли це знадобиться для обліку, доведеться його скопіювати, імпортувати в різні функції (що заборонено) або підняти його у вищий рівень.
    • Розділення проекту на окремі функції занадто рано завдає ще більшої шкоди, ніж занадто раннє абстрагування. Розміщення застосунку з 40 файлами у п’яти окремих папках функцій додає лише формальностей, а не структури.
  • Конфігурація lint не масштабується сама по собі. Правило cross-feature в Bulletproof React вимагає окремої зони для кожної функціональності, причому кожна з них має бути виключена з перевірки. Десять функцій означають десять вручну створених зон, і якщо про одну забути, ця функціональність залишиться незахищеною.
  • Де виникають проблеми

    Здебільшого проблем немає, саме тому цей підхід є стандартним. Він стає проблематичним у двох ситуаціях. По-перше, коли одна функціональність стає дуже великою: папка checkout/ з 80 файлами є лише структурою на один рівень нижче, заснованою на типах. По-друге, коли з’являється справжня модель домену, яка не узгоджується з функціями інтерфейсу. Ці два фактори призводять до формування наступних трьох структур.

    Atomic Design

    Що це таке

    Atomic Design — це методологія Бреда Фроста, представлена у блог-пості в червні 2013 року та розроблена детальніше у книзі 2016 року. Вона визначає п’ять рівнів. Перефразовуючи його визначення:

    • Атоми — це базові елементи, з яких складається кожен інтерфейс.
    • Молекули — це досить прості групи елементів користувацького інтерфейсу, які працюють разом як єдине ціле.
    • Організми — це більш складні компоненти, створені з молекул, атомів та навіть інших організмів.
    • Шаблони — це структури на рівні сторінки, які розміщують компоненти у певному форматі та описують структуру вмісту.
    • Сторінки — це конкретні приклади шаблонів, заповнені реальним, типовим вмістом.

    Деталь, яку майже всі ігнорують, — це власний опис мети Frost. Він представляє Atomic Design як ментальну модель, а не лінійний процес: спосіб бачити інтерфейс водночас як цілісну структуру та сукупність окремих частин. Це ніколи не була структура папок; спільнота додала її пізніше.

    Хто його використовує

    Тут реальність відрізняється від репутації. Існує мало підтверджених доказів того, що великі системи дизайну React використовують точну таксономію папок atoms/, molecules/, organisms/. Наприклад, Shopify Polaris та GitHub Primer не використовують таку номенклатуру, попри те, що результати пошуку можуть свідчити про протилежне. Очевидним прикладом є Pattern Lab — власний інструмент Frost, створений для демонстрації цього підходу.

    Як термін у командах систем дизайну, Atomic Design має величезний вплив. Однак як точна структура папок у продакшн-застосунках на React докази її використання значно слабкіші, ніж свідчить її популярність. Якщо хтось стверджує, що велика компанія організовує свої папки в React саме так, запитайте показати репозиторій.

    Переваги

    • Це надає дизайнерам та інженерам спільну мову. Це справжня перевага, і папки тут не відіграють жодної ролі. Якщо дизайнер посилається на певний елемент, інженер одразу розуміє його приблизний обсяг, що є корисним у командах з розробки дизайн-систем.
    • Це заохочує композиційний підхід. Ієрархія спонукає створювати невеликі елементи та їх об’єднувати, що є хорошою практикою у React незалежно від того, де знаходяться файли.
    • Це відкриває список примітивних елементів. Папка atoms/ чітко показує, коли у вас є три варіанти кнопки, які насправді мають бути однією.

    Недоліки

    • Класифікація справді складна, і її автор це усвідомлює. Modal.jsx поєднує фон, контейнер, заголовок та елемент для закриття. Чи є це групою атомів, а отже молекулою, чи складною самостійною одиницею, а отже організмом? Зрештою Фрост оприлюднив орієнтовне правило: якщо розбиття компонента дає базові елементи, такі як зображення, заголовки, абзаци та кнопки, це молекула; якщо ж воно дає менші компоненти, такі як картки, списки для поширення чи медіаоб’єкти, це організм. Такий евристичний підхід існує лише через неоднозначність меж, а кожна неоднозначна межа стає предметом постійних дискусій, які не приносять жодної користі користувачеві.
  • Він класифікує лише інтерфейс користувача, тому не може структурувати всю програму. Це є критичним недоліком. Atomic Design має погляди щодо Button та CheckoutForm, але зовсім не має поглядів щодо useCheckout, checkoutApi, cartStore чи formatPrice. Половина файлів не потрапляє під цю таксономію, тож команди опиняються з атомами, молекулами та організмами, а також з типовою структурою для всього іншого, що призводить до поєднання обох наборів проблем.
  • Цей підхід прямо суперечить принципу колокації. Усе, що пов’язано з процесом оплати, постійно розкидане між папками organisms/, templates/, pages/, hooks/ та api/.
  • Компоненти піднімаються у рангу, тож файли переміщуються. Молекула трохи зростає, стає організмом, переходить у іншу папку та руйнує всі імпорти. Таксономія залежить від складності, а складність змінюється.
  • Власна документація React застерігала від сприйняття її як суворого правила, а не як корисного прикладу.
  • Де це не спрацьовує

    Як тільки цей підхід виходить за межі системи дизайну. Впорядкування примітивів за складністю всередині packages/ui/ або shared/ui/ є доцільним. Однак у масштабах всього додатку цей підхід одразу ж зазнає невдачі, адже основна частина будь-якого додатку складається з елементів, що не є компонентами інтерфейсу.

    Власна позиція Фроста підтримує таке тлумачення: він називає це ментальною моделлю, а не догмою, та пропонує командам змінити назви етапів. Команди, які суворо дотримуються принципів атомів та молекул у React-додатку, є більш жорсткими, ніж сам творець цього методу.

    Структура, заснована на домені

    Що це таке

    Підхід Domain-Driven Design походить з книги Еріка Еванса 2003 року, написаної більш ніж за десять років до появи React, і є способом моделювання складних бізнес-доменів у коді. Два з його принципів застосовуються також у фронтенді. Обмежений контекст — це частина програмного забезпечення, у якій певні терміни, визначення та правила застосовуються послідовно. Принцип універсальної мови полягає у тому, що код має використовувати ту саму лексику, що й бізнес.

    У застосуванні до React-додатку кожен домен стає верхнім каталогом, який всередині поділяється на шари:

    • domain/ містить ентитети та об’єкти значень: суто бізнес-правила.
    • application/ містить кейси використання.
    • infrastructure/ містить клієнтів API та системи зберігання.
    • presentation/ містить компоненти React.

    Залежності вказують лише всередину, тому домен нічого не знає про React чи HTTP.

    Подивіться, що відбувається з незручними файлами. formatPrice більше не є допоміжною функцією; він перетворюється на об’єкт значень Money, який знаходиться в catalog/domain/. useAuth скорочується до простого обгортка рівня представлення над кейсом використання loginUser. React навмисно відсувається на периферію.

    Хто це використовує

    Найкращим практичним довідником є стаття Олекса Беспоясова Clean Architecture on Frontend, яка містить повну реалізацію на React та TypeScript у репозиторії bespoyasov/frontend-clean-architecture. Він пояснює, що сфера домену — це та частина, яка залишається незмінною незалежно від того, чи переходите ви з React на Angular, чи змінюєте певний сценарій використання.

    Також варто знати, що Манфред Штайєр, можливо, найвідоміший прихильник підходу DDD у фронтенді, переважно працює з Angular. Модель Angular, заснована на класах та орієнтована на сервіси з інтенсивним використанням ін’єкцій залежностей, є більш природним середовищем для DDD, ніж функції та хуки в React. Перш ніж повністю прийняти якісь чіткі рекомендації щодо DDD у фронтенді, переконайтеся, для якої саме фреймворк-системи вони були написані.

    Цей стиль використовується у сферах фінтеху, страхування, охорони здоров’я, логістики та корпоративного програмного забезпечення типу B2B, де бізнес-правила є складними та існували задовго до появи користувацького інтерфейсу.

    Переваги

    • Бізнес-логіка залишається актуальною після зміни фреймворку. Об’єкт Cart із правилами addItem — це звичайний TypeScript; заміна React не вплине на рівень домену. Для продукту, який має прослужити десять років, це є значним активом.
    • Правила можна тестувати без React. Немає нічого для відображення, жодних хуків та бібліотек для тестування — лише функції та перевірки, які виконуються за мілісекунди. За наявності складних правил це саме по собі може бути підставою для такої структури.
    • Код відображає бізнес-діалог. Коли експерт у галузі каже, що замовлення не може бути відправлене, доки не буде перевірена адреса, це правило має лише одне місце зберігання — файл під назвою Order.ts.
  • Обмежені контексти підходять для великих організацій, адже кожен контекст може належати одній команді з чітким інтерфейсом до інших.
  • Недоліки

    • Обсяг необхідних елементів коду є значним, і це є основною витратою. Кожна ентріта зазвичай передбачає модель домену, DTO та мапер між ними: три файли та шар перекладу для того, щоб функціонал додатку був представлений у вигляді єдиної типованої відповіді API. Це потрібно множити на двадцять ентрит.
    • Більшість додатків на React не мають справжньої доменної моделі. Це скоріше судження, ніж підтверджений факт, але саме це є ключовою ідеєю цього розділу. Типовий додаток на React відображає дані з API, яким володіє інша команда чи сервіс, і справжня складність полягає у стані сервера, кешуванні, оптимістичних оновленнях та умовах конкуренції. PDD не вирішує жодної з цих проблем; бібліотека для керування станом сервера, така як React Query, краще вирішує фактичну проблему, ніж шар доменної моделі. Якщо ваші ентитети — це типові відповіді API без інваріантів, архітектура коштує грошей та нічого не захищає.
    • Фронтенд насправді не може уникнути своєї фреймворк-архітектури. Обіцяється незалежність від фреймворку, але фронтенд завжди прив’язаний до моделі відображення, механізмів маршрутизації, патернів керування станом, API браузера та інструментів для компіляції. Чиста архітектура переміщує ці обмеження на периферію, а не усуває їх. Це справжня перевага, хоча й менша, ніж стверджується.
    • Контексти користувацького інтерфейсу є нечіткими. На бекенді чітко визначено власників даних. Фронтенд відображає те, що потрібно екрану, і одна сторінка часто охоплює три контексти, тож межі стають суб’єктивними та постійно змінюються.
    • Інтеграція нових співробітників займає час. Розробнику на React, який ніколи не стикався з DDD, може знадобитися кілька тижнів, щоб стати продуктивним, і хтось завжди буде запитувати, навіщо взагалі існує мапер.

    Де виникають проблеми

    Коли домен існує лише на папері. DDD розроблений для того, щоб принести користь у ситуаціях справжньої бізнес-складності. Якщо ваші інваріанти є реальними — наприклад, гроші, контракти, регульовані процеси або багатокрокові машини станів, керовані правилами, — інвестиції окупаються. Коли завдання полягає у відображенні списку продуктів з певного кінцевої точки, ви просто додаєте безліч формальностей навколо операції fetch.

    Корисний тест: чи можете ви назвати три бізнес-правила, які є цікавішими за перевірку обов’язкових полів? Якщо ні, то у вас є лише інтерфейс користувача, а не домен.

    Дизайн з функціональними шматками

    Що це таке

    Дизайн з функціональними шматками (Feature-Sliced Design, FSD) — єдина структура тут, яка є формальною специфікацією, що включає версійні релізи, документацію та офіційний інструмент перевірки архітектури під назвою steiger. Інші є лише конвенціями; FSD записана письмово.

    У ньому є три рівні ієрархії:

    • Шари — це фіксовані верхній рівень папок: app, pages, widgets, features, entities та shared.
    • Сегменти — це групи всередині шару, розділені за бізнес-значенням, тобто за тим, що код означає для продукту чи додатку.
    • Частини — це групи всередині сегмента, розділені за технічною метою: ui, api, model, lib та config.

    Правило, яке утримує все разом, полягає у тому, що модуль у сегменті може імпортувати інші сегменти лише тоді, коли вони знаходяться на шарах, які є суворо нижчими за його власний. Імпорти завжди спрямовані вниз, і два сегменти, які ділять шар, ніколи не можуть імпортувати один одного. Між сусідніми елементами немає жодного зв’язку, що забезпечується інструментом перевірки коду, а не добрими намірами.

    Дві корекції тверджень, які поширюються в інших джерелах, варто знати:

    • Шар processes вже не підтримується. У документації зазначено, що поточні специфікації рекомендують уникати його та переміщувати його вміст у features та app. Керівництва, які досі перераховують сім шарів, включаючи processes, описують старішу версію.
    • Шар pages не був відмінений; його рівень було підвищено. У версії 2.1, випущеній у листопаді 2024 року, є заголовок „Сторінки на першому місці!“ Тепер рекомендується зберігати більше коду всередині сторінок: великі блоки інтерфейсу, форми та логіку обробки даних, якими не користуються інші сторінки, мають залишатися у відповідному фрагменті цієї сторінки.

    У версії FSD цього прикладного додатку файл CheckoutForm.tsx знаходиться у директорії pages/checkout/ui/, а не у окремому модулі функцій. Підхід версії v2.0 з принципових міркувань передбачав розділення його на ентитети та функції, тоді як версія v2.1 залишає його всередині сторінки, поки інша сторінка фактично не потребує його частини.

    Мотивація цієї зміни є найцікавішою. Розбиття на ентитети, характерне для версії v2.0, призводило до слабкої кохезії: для відстеження одного потоку користувача доводилося пройти через багато папок, а знайти непотрібний код було складно. Розробники фактично визнали, що попередня версія вимагала занадто багато архітектурних рішень ще до написання будь-якого коду. Таким чином найсерйозніша критика FSD була висловлена та вирішена самими його авторами. Слід визнати заслуги за впровадження корекцій, але команди, які використовували v2.0, заплатили за цей урок.

    Хто його використовує

    Ситуація з впровадженням потребує чесного аналізу. На момент написання сторінки головної страниці FSD містить розділ для компаній, які використовують FSD, де не наводиться жодних прикладів, а замість цього пропонується відвідувачам повідомити про свою компанію.

    На офіційній сторінці з прикладами перелічено близько 21 проекту, переважно демонстраційні версії та додатки для хобі, такі як клон RealWorld, гра Sudoku, фейковий інтернет-магазин та невелика браузерна гра. Проекти, які схожі на серйозне програмне забезпечення, є незначними за обсягом – це Roke.to, Moke Smoke, Moonlogs та розширення для Chrome.

    За даними на момент написання, показники є скромними, але реальними: близько 297 репозиторіїв у GitHub використовують тему feature-sliced-design, репозиторій документації має близько 2,3 тис. „зірок“, а інструмент для перевірки коду – близько 388. Спільнота зосереджена у середовищі російськомовних розробників, через що кількість використання у англомовному середовищі виглядає меншою, ніж є насправді.

    З первинних джерел не вдалося підтвердити, що якась велика, відома західна компанія використовує FSD, а жодна офіційна документація не вказує автора цієї методології. Для настільки формалізованого інструменту це незвичайно та варто враховувати під час прийняття рішення про його використання.

    Переваги

    • Вона усуває суперечки щодо розміщення елементів, адже специфікація вже прийняла рішення. Куди покласти цей файл? Специфікація відповідає. Це економить час у великих командах, і саме тут FSD значно перевершує підхід, заснований на функціях, у якому визначення кожної функції постійно переглядаються.
    • Межі перевіряються машинами. steiger та конфігурація ESLint забезпечують дотримання правил імпорту, а архітектура, яка підконтрольна цим правилам, розвивається набагато повільніше, ніж та, що не підконтрольна.
    • Цей підхід підходить для великих команд із мінімальною координацією. Фіксовані шари та відсутність імпортів між елементами дозволяють командам працювати паралельно без конфліктів.
  • Якщо правильно розуміти розділення на ентитети та функції, це значно спрощує справу. product — це щось, що існує; add-to-cart — це дія, яку виконує користувач. Це різні типи коду, а більшість схем їх змішують.
  • Слабкості

    • Крива навчання є досить крутою, і на власній сторінці з мотивацією FSD визнає цю загальну проблему, зазначаючи, що існуючі підходи мають високий поріг входу, що ускладнює найм спеціалістів. FSD не є винятком: перед тим, як можна буде розмістити хоча б один файл, потрібно опанувати три таксономії — шари, сегменти та частини.
    • Зміна на версію v2.1 свідчить про зміни в підходах. Змінилася основна одиниця розбиття, тож кожен, хто працює за специфікаціями, змушений слідувати за цими змінами.
    • Власна документація вважає цей підхід надмірним для невеликих проектів, зазначаючи, що він не потрібен для короткострокових проектів або тоді, коли швидкість важливіша за якість коду.
    • Доказів ефективності використання недостатньо. Це підходить для особистих проектів, але складно переконати зацікавлених сторін, коли вони запитують, хто ще керує проектом у режимі продакшну.
    • Міграція застарілої бази коду є дорогою. Додавання нових шарів та компонентів до існуючого коду — це масштабний проект із мінімальною помітною вигодою.

    Де це призводить до проблем

    У малих командах та короткострокових додатках, де формальності переважають над реальною користю. FSD орієнтований на великі, стабільні продукти з багатьма учасниками, яким справді потрібні чіткі межі. Для менших проектів структура, заснована на функціях, дає більшу частину переваг при значно меншій кількості концепцій.

    Модульна структура та монорепозиторії

    Що це таке

    На відміну від інших варіантів, тут не йдеться про організацію структури src/, тому це розглядається останнім. Йдеться про логіку структурування репозиторію: кілька додатків та спільних пакетів у одному репозиторії, які координує виконавець завдань.

    У документації Turborepo описується стандартна форма: спочатку розміщують додатки та сервіси у папці apps/, а все інше, таке як бібліотеки та інструменти, — у папці packages/. Дивіться посібник Turborepo з створення структури репозиторію.

    Кожен додаток у папці apps/ все одно потребує внутрішньої структури з одного з інших шести варіантів. Monorepo є окремою концепцією від способу організації окремого додатку, і плутанина між ними є найпоширенішим непорозумінням у цій темі.

    На момент написання цього тексту основними інструментами були turbo (версія 2.10.x, приблизно 73 мільйони щомісячних завантажень), nx (версія 23.x, приблизно 38 мільйонів) та lerna (версія 9.x, приблизно 7,5 мільйонів). Перевіряйте актуальні цифри, перш ніж покладатися на них. На відміну від поширеного переконання, Lerna не вважається застарілим. У 2022 році Nx взяв на себе його управління, оскільки існувала небезпека того, що його більше не будуть підтримувати. Він продовжує розроблятися, а тепер передає функції виконання завдань та кешування Nx, зосереджуючись при цьому на версіонуванні та публікації.

    Хто його використовує

    Під час обговорень монорепозиторіїв часто згадують Google, Meta та Microsoft, що є оманливим. Це монорепозиторії всієї компанії, а не монорепозиторії React, і це нічого не говорить про ваш додаток на React.

    Ці цифри все ще цікаві. За даними, у репозиторії Google було приблизно мільярд файлів та 86 ТБ (Potvin і Levenberg, Communications of the ACM, липень 2016 року). Meta описує свій репозиторій, яким керує Sapling, як містящий десятки мільйонів файлів, комітів та гілок. За словами Браяна Гаррі у травні 2017 року, репозиторій Windows у Microsoft містив близько 3,5 мільйона файлів об’ємом 300 ГБ, над якими працювали приблизно 4 000 інженерів.

    Кожна з цих компаній була змушена створити власні інструменти для роботи з цим: Google розробив Piper та Bazel, Meta створила Sapling та EdenFS, а Microsoft написала GVFS. Розглядайте це як сигнал про витрати, а не як схвалення. Якщо монорепозиторію потрібна спеціалізована команда інфраструктури для функціонування, це щось говорить про монорепозиторії.

    Відповідні, перевірені приклади з React відрізняються:

    • Репозиторій Vercel з next.js є справжнім React-монорепозиторієм із каталогами apps/ та packages/ на верхньому рівні, просторами роботи pnpm та 19 пакетами, серед яких next, create-next-app та eslint-config-next.
    • Shopify зберігає свої додатки React Native в одному монорепозиторії, який охоплює Shop, Inbox, Point of Sale, Shopify Mobile та Local Delivery. Їхня інженерна команда пояснила у вересні 2021 року, що один монорепозиторій знижує витрати на технічне обслуговування, оскільки потрібно вдосконалювати та оптимізувати лише один процес, а не підтримувати багато окремих пакетів та репозиторіїв.

    Ось справжня причина використання монорепозиторія, і слід зазначити, що це стосується процесу технічного обслуговування, а не організації коду.

    Переваги

    • Зміни між пакетами є атомарними. Зміна packages/ui/Button та всіх трьох додатків, які ним користуються, відбувається в одному коміті, одному запиті на пул-реквест, одній перевірці та одному запуску CI. З окремими репозиторіями це перетворюється на підвищення версії, публікацію та три оновлення залежностей. Саме тому існують монорепозиторії.
    • Спільне використання має чіткі межі. packages/ui має власний файл package.json та чітко визначений публічний API, причому межа пакету є найсильнішим інструментом у JavaScript.
    • Існує лише одна версія всього: жоден додаток не застрягає на React 18, поки інший працює з версією 19 — є один файл lockfile та одне джерело істини.
    • Великі рефакторинги стають можливими. Ви можете змінити назву API у всіх додатках, які ним користуються, в одному коміті, при цьому компілятор перевірить отриманий результат.

    Слабкості

    • Інструментарій є справжнім та постійним тягарем: робочі простори, графи завдань, віддалене кешування та матриці CI. Власна документація Turborepo визнає проблему масштабування, адже кожен робочий простір має власні завдання на тестування, перевірку коду та збірку, і один монорепо може мати тисячі таких завдань. Turborepo створений для вирішення проблем, які створюють монорепо.
    • Процес CI стає повільнішим та дорожчим, перш ніж прискоритися. Без належно налаштованого виявлення змін та кешування кожен pull request виконує все це.
    • Інтеграція нових співробітників є складнішою. Перед першим комітом новачок мусить зрозуміти, як працюють робочі простори, як функціонує граф завдань та як поводиться кешування.
  • Спільні пакети створюють зв’язок, який маскується під повторне використання. Як тільки у packages/ui з’являється три користувачі, кожна зміна ризикує спричинити проблеми у трьох додатках. Команди часто реагують додаванням параметрів та варіантів, поки спільний компонент не перетворюється на мову конфігурації, яку ніхто не розуміє.
  • Де це призводить до проблем

    Коли є лише один додаток. Монорепозиторій, у якому папка apps/ містить один додаток, оплачує повну вартість інструментарію без жодної вигоди: немає коду для спільного використання та неможливо здійснити атомарну зміну між додатками.

    Щодо порогових значень, то жодних авторитетних даних не існує. Turborepo та Nx не публікують інформації про розмір команди чи кількість додатків, при яких використання цих інструментів стає доцільним. Точні цифри, які поширюються в Інтернеті, наприклад, конкретна кількість інженерів, яка нібито вимагає використання Turborepo, походять з низькоякісного контенту для SEO, а не з документації виробника чи наукових досліджень. Будь-хто, хто наводить точний поріг, просто його вигадує.

    Розумне правило, яке слід розглядати як думку, а не як факт: використовуйте монорепо, коли у вас є два справжніх додатки, які дійсно ділять код, або один додаток разом із системою дизайну, якою користуються зовнішні команди. У інших випадках витрати перевищують користь, і ваша власна ситуація може змінити цей баланс.

    React Server Components не визначають структуру папок

    Багато нещодавніх статей стверджують, що React Server Components змінюють спосіб структурування папок, зазвичай рекомендуючи окремі каталоги components/server/ та components/client/. Офіційна документація не підтримує це.

    Директива use client позначає межу в графі модулів, а не у дереві каталогів. Відповідний посібник з рендерингу Next.js описує use client як засіб для визначення межі між графами модулів сервера та клієнта, пояснюючи, що як тільки файл містить цю директиву, його імпорти та всі компоненти, які він безпосередньо відображає, потрапляють до клієнтського бундлу.

    Цей другий момент є ключовим. Межа поширюється через імпорти, а не через папки. Файл у каталозі client/ не є компонентом клієнта через своє розташування, а файл у каталозі server/ не отримує жодного захисту через свою назву. Те, що визначає результат, — це директива та ланцюжок імпортів.

    Якщо ви хочете забезпечити дотримання правил, існують npm-пакети server-only та client-only, які спричиняють провал процесу будування, коли код перетинає неправильну межу. Папки цього не забезпечують.

    Офіційні рекомендації насправді спрямовані в протилежному напрямку: переміщуйте 'use client' вниз по ієрархії, до листових компонентів, щоб зменшити розмір пакету клієнта. Розділення на верхньому рівні на папки сервера та клієнта спонукає до протилежного підходу.

    Кілька конвенцій App Router дійсно впливають на організацію коду, і вони є реальними:

    • Групи маршрутів, позначені як (folder), організовують маршрути без зміни URL.
    • Приватні папки, позначені як _folder, повністю виключаються з процесу маршрутизації.
    • Колокейшн за замовчуванням є безпечним, оскільки маршрут стає загальнодоступним лише після додавання файлу page.js або route.js. Компоненти, тести та утиліти можуть знаходитися безпосередньо всередині сегментів маршруту, не стаючи маршрутами, тому для цього приватні папки не потрібні.

    Next.js чітко зазначає, що не має думки щодо того, як організовувати та розміщувати файли проекту. Його документація пропонує три стратегії організації, не ранжуючи їх.

    Як вибрати

    Ось рішення у стислій формі з найважливішими аргументами:

    • Кількість файлів менше приблизно 30, або ви ще досліджуєте проблему: краще залишитися з простою структурою. Структура, заснована на реальних закономірностях змін, краща за ту, яку припускають відразу з самого початку, як і пропонували документація React.
    • Коли кількість файлів менше приблизно 50, команда невелика та проект має короткий термін існування, підходить структура, заснована на типах. Хоча це вже не модно, це не має значення; міграція коштуватиме більше, ніж допоможе.
    • Для будь-яких реальних проектів з довгим терміном існування краще використовувати структуру, засновану на функціях. Це має бути стандартом. Вона задовольняє принципи Screaming Architecture, сприяє ефективному розміщенню компонентів, уможливлює просте видалення функцій та підтримує обмеження, встановлені інструментами lint. Почніть з одинадцяти папок у проекті Bulletproof React, рано додайте зони import/no-restricted-paths та ігноруйте файли типу barrel.
  • Велика команда, продукт з довгим терміном існування та постійні суперечки щодо того, що вважається функцією: подивіться на FSD. Ви купуєте специфікацію, яка вже прийняла рішення, та інструмент для їх контролю, за ціною — слабкі докази впровадження та велика кількість концепцій.
  • Справді складні бізнес-правила, які існують довше за користувацький інтерфейс: додайте шар домену, а не всю структуру DDD. Перемістіть правила у звичайні ентитети TypeScript, які ніколи не імпортують React, та залиште все інше заснованим на функціях. Це дозволить отримати більшу частину цінності при значно менших зусиллях.
  • Система дизайну з реальними користувачами: використовуйте Atomic Design лише всередині цього пакета як словниковий запас. Не включайте його до загального макету додатку.
  • Кілька справжніх додатків, які використовують однаковий код: слід застосувати монорепозиторій із папками apps/ та packages/, який керується Turborepo або Nx, і дозволити кожному додатку обирати власну внутрішню структуру з цього списку.
  • Є одне правило, яке має перевагу над усіма іншими: підходи поєднуються. Реальні кодові бази змішують їх, і у документації React про це також зазначалося, підкреслюючи, що у більших проектах на практиці часто використовується поєднання різних підходів, тому вибір „правильного“ з самого початку не є надто важливим.

    Цілком розумна конфігурація для продакшну — це монорепозиторій, який містить додатки, засновані на функціях, пакет системи дизайну, організований за принципом атомарних елементів, та шар домену у тій частині, де знаходяться справжні бізнес-правила. Ніхто не оцінює вашу архітектуру за її чистотою.

    Основні висновки

    Люди, які найближче працюють із цими стандартами, дотримуються їх вільно та постійно внескають зміни у їхню версію публічно. Фрост називає Atomic Design ментальною моделлю, а не догмою. У документації React час прийняття рішення було обмежено п’ятьма хвилинами, а потім сторінка була видалена. Next.js та Vite ухиляються від займання позиції. FSD змінив свою основну одиницю розбиття на частини у версії v2.1, а Bulletproof React змінив свою рекомендацію щодо файлів у форматі barrel. Вони змінили підхід, тому що прислухалися до реальних відгуків, а не намагалися захистити свою позицію.

    Це залишає два стабільні правила, обидва старші за будь-яку схему розташування, про яку йдеться тут:

    • Screaming Architecture: верхній рівень папок має пояснювати новому інженеру, що робить продукт, а не яку фреймворк-систему він використовує.
    • Colocation: елементи, які змінюються разом, мають знаходитися якомога ближче один до одного.

    Кожна структура в цьому посібнику намагається дотримуватися цих двох правил у певному масштабі. Виберіть найдешевший варіант, який задовольняє їх для вашого поточного проекту, а не той, яким ви сподіваєтесь його зробити. Сигнали про необхідність змін є конкретними: префікси імен файлів замість папок, чотири відкриті папки для однієї зміни чи година, витрачена на обговорення того, чи є щось молекулою. Дійте на основі цих сигналів, коли вони з’являються, а не раніше.