Чому більшість «перевикористаного» React-коду перетворюється на over-engineering і як правильна архітектура вирішує цю проблему

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

React — це бібліотека, яка просуває компонентний підхід. Але чи завжди є сенс у тому, щоб ускладнювати все до межі?

На рев’ю можна почути:

— «Цей компонент на 400 рядків — треба розбити.»

— «На що саме розбити? І навіщо?»

— «Ну як на що, на менші компоненти. Щоб можна було пере використовувати. І ще ця функція на 60 рядків — винеси в окрему!»

Знайомо? Вітаю, ви в світі, де магічні цифри важливіші за здоровий глузд. Де армія розробників створює мікрокомпоненти не заради реальної користі, а просто «бо так треба». І в результаті, відкриваючи папку components, ви частіше думаєте: «Простіше написати заново», ніж шукати щось готове.

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

Магічні числа

У спільноті давно сформувалися «правила», які передаються від ментора до джуна:

Для компонентів:

  • не більше 100 рядків,
  • не більше 5 пропсів,
  • всю логіку — в хуки,
  • максимум 3 рівні вкладеності JSX.

Для функцій:

  • одна функція = одна дія,
  • максимум 20 рядків,
  • максимум 3 параметри,
  • кожну абстракцію — в окрему функцію.

Питання: а звідки ці цифри? Хто їх довів?

Насправді все дуже відносно.

Типова сцена: форма реєстрації

Розробник пише складну форму з динамічно змінними полями за допомогою React Hook Form. Вже є готовий компонент Input, який обробляє всі стандартні пропси, валідацію і помилки. Всі поля, валідація з Yup, обробка помилок — все логічно. Код займає 400 рядків.

На рев’ю: «Занадто велика. Треба розбити.»

Результат: 10 мікрокомпонентів і 10 мікрофункцій, кожен викликає інший. І вся ця форма використовується тільки в одному місці. Щоб зрозуміти, як працює форма, треба відкрити 12 файлів і прослідкувати 15 рівнів викликів.

Навіть використання готового Input не врятувало ситуацію — надмірна декомпозиція лише додала шари абстракції та складність.

Вітаю, ви перетворили 400 рядків зрозумілого коду на архітектурний хаос.

Реальний приклад: сайдбар адмінки

Складний сайдбар із навігацією, пошуком, списком проєктів, сповіщеннями, налаштуваннями. 500 рядків коду.

Підхід: 15 мікрокомпонентів


src/components/sidebar/
├── SidebarContainer.jsx
├── SidebarHeader.jsx
├── SidebarNavigation.jsx
├── SidebarNavigationItem.jsx
├── SidebarSearch.jsx
├── SidebarProjectList.jsx
├── SidebarProjectItem.jsx
├── SidebarNotifications.jsx
├── SidebarUserMenu.jsx
└── ... ще 6 файлів

Проблеми:

  • props drilling через 4 рівні,
  • щоб додати фічу, треба вивчити 15 файлів,
  • 90% компонентів ніхто не пере використовує,
  • дебаг перетворюється на квест.
  • додаткові інтерфейси для пропсів

Функції-шари

Функції мають бути чистими. Це звучить правильно... доки не дійде до практики.

Функція на 60–70 рядків, яка робить одну справу, — це нормально. Наприклад: handleSubmit, яка працює з loading-станом, виконує валідацію, ловить помилки, відправляє запит. Вона велика, але цілісна.

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

Фактично, ви отримуєте «функцію-шару», де логіка розкидана по десятках дрібних функцій, але кінцевий результат той самий. Іноді це навіть ускладнює дебаг і робить код менш читабельним.

YAGNI

Ще одна ключова проблема — ігнорування принципу YAGNI (You Aren’t Gonna Need It).

💡 Якщо компонент чи функція використовується лише в одному місці, а ви «про всяк випадок» виносите його у спільну бібліотеку — ви не економите час, ви його витрачаєте.

Кожен такий «одноразовий» компонент коштує дорого:

  • потрібно продумати API та пропси,
  • підтримувати сумісність,
  • писати тести,
  • пояснювати колегам, як це використовувати.

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

Наслідок: проект стає складнішим, новачки довше проходять онбординг, швидкість розробки падає.

Правило просте:

  • використовується у двох і більше місцях — виносимо,
  • лише в одному — залишаємо локально.

Бібліотеки компонентів

«У нас є бібліотека, все повинно бути універсальним» — знайомо.

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

SOLID і «малі» компоненти

Якщо ви обираєте підхід із дрібними мікрокомпонентами, варто розуміти, що це може створювати конфлікт із принципами SOLID.

  • Принцип S (Single Responsibility) — компонент має відповідати лише за одну річ. Деколи, здається, що маленькі компоненти допомагають дотримуватися цього принципу. Так, з одного боку, він підтримується.
  • Але ось проблема: надмірна декомпозиція часто порушує принцип I (Interface Segregation). Кожен новий компонент потребує свого API, нових пропсів, типів, тестів. У підсумку замість простої функції чи компонента ви отримуєте мереживо інтерфейсів і типів, яке треба підтримувати, тестувати і документувати.

Інакше кажучи, намагання «розбити все на атоми» формально дотримується S, але водночас тягне за собою порушення I. Тобто ви витрачаєте час на інфраструктуру типів замість того, щоб працювати над реальною бізнес-логікою.

дотримання SOLID — це важливо, але не треба робити це за будь-яку ціну.

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

Рішення: правильна архітектура

Хороша архітектура вирішує більшість проблем. Не потрібно ділити все на атоми. Потрібна розумна структура.

Підхід 1: Feature-Sliced Design (FSD)

src/
├── shared/          # Реально багаторазове
│   ├── ui/          # Button, Input, Modal
│   └── lib/         # utils, hooks
├── entities/        # Бізнес-сутності
│   ├── user/
│   └── project/
├── features/        # Функціональність
│   ├── auth/
│   │   └── ui/
│   │       └── RegistrationForm.jsx  # 400 рядків - OK бо тут він збирається з шарів вище
│   └── project-list/
└── pages/           # Сторінки

Принцип: компонент живе там, де використовується.

Підхід 2: Atomic Design


src/
├── components/
│   ├── atoms/       # Button, Input
│   ├── molecules/   # FormField, SearchBar
│   └── organisms/   # Sidebar, Header (можуть бути великими!)
├── features/
│   └── registration/
│       └── RegistrationForm.jsx  # 500 рядків - нормально
└── pages/

Принцип: складний organism у 500 рядків — це нормально.

Підхід 3: Структурований моноліт під проект

src/
├── components/      # Загальні компоненти
├── modules/
│   ├── dashboard/
│   │   ├── components/
│   │   │   └── DashboardWidget.jsx
│   │   └── Dashboard.jsx  # 600 рядків - OK
│   ├── projects/
│   │   ├── components/
│   │   └── ProjectList.jsx
│   └── admin/
│       ├── components/
│       │   └── AdminSidebar.jsx  # 500 рядків - нормально
│       └── AdminPanel.jsx
└── utils/           # Реально пере використовувані функції

Принцип: у кожному модулі своя папка components/. У корені — лише те, що використовується у 3+ місцях.

Особистий досвід

Розкажу вам історію...

В одному проєкті ми робили величезну CRM. Зустріти компонент на 2000 рядків, було абсолютно нормально, через масштаб проекту який розроблявся вже багато багато років.

В іншому проєкті ми використали мікросервіси. Та як на мене гіпердекомпозицію. Користі не було. Чесно кажучи, працювати з CRM було простіше.

Але уявіть, якби на першому проекті розбивали все так само, як у другому, ми б просто помножили багатошаровість архітектури на два. Толку з цього — нуль, а от джуни, які б у все це занурювались, швидко зійшли б з розуму. Це ще при тому що там навіть тайп скрипта не було 🤫

Висновок

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

Перестаньте ділити компоненти та функції «бо так треба».

Компонент на 400 рядків, де все зрозуміло, — це нормально. Функція на 70 рядків, яка робить одну справу, — теж нормально.

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

💡 Хороша архітектура — це коли у тебе рівно стільки, скільки потрібно. Не більше й не менше.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

плюсую люто. справедливості заради — не бачив особливо числових саме обмежень окрім хіба не більше 4х параметрів функції.
як на мене, ще одне безумство не згадане, яке дуже рано з’явилося в реакт-проектах — реекспорти всього і вся через barrel files (index.js/ts). у підсумку — купа тек, де лежить 1-2 файли і тут же реекспорт, при тому він використовується теж 1-2 рази. зате кількість файлів і бойлерплейту зростає десь на чверть.

дякую. повністю згоден — index.js/.ts як public API дуже класна штука, коли її використовують за призначенням. проблема в тому, що можно побачити один index.ts на весь проект, де 100+ рядків імпортів. а насправді це має працювати в рамках інкапсуляції — щоб обмежити імпорти тільки конкретних речей з конкретного шару/модуля. де мі не експортуємо все підряд, а свідомо вибираємо публічний API і ховаємо внутрішню реалізацію. тоді це реально класний патерн

Дякую за змістовну статтю. Дуже доступно пояснено, навіть складні речі стають зрозумілими 👍

Нарешті хоч хтось про це написав, дякую :). Все ніяк не знайду час написати статтю «Що не так з Реакт», в якій хотів пройтися по цих сталих практиках, що генерять складність на рівному місці. Ну і також по Redux за оверінжиніринг заради оверінжиніринга. На щастя, зараз вже у фронтовий код нечасто доводиться дивитися :).

Дякую!)
А щодо Redux — оверінжиніринг це окрема біль:) Добре, що веб розробка рухається в бік більш зручних способів керувати глобальним стейтом

Про це ще дядько Боб Роберт Мартін написав, так само скажімо його колега Мартін Фаулер. От що добре тут про це написали з практичним застосуванням на одному із тех стеків які є популярними на сучасному ринку.Технології міняються під потреби бізнесу і нові можливості Hardware та інфраструктури, наприклад швикдкості доступу до мережі Internet, але не міняються принципи на яких вони побудовані. В тому числі принципи створення кодової бази так, щоби програмний продук підлягав подальшій підтримці — що є простота внесення змін в нього як то додавання або прибирання нової фунціолнальності, так і швидкості локалізації та виправлення знайдених дефектів.

Саме так, у підсумку це все зводиться до простоти внесення змін

Розбивати варто не заради міфічної «перевикористовуваності», яка десь так само реальна, як і єдинороги, а заради читабельности. Коли я бачу компонент на 400 рядків, мені млосно стає, бо поки розберешся хто на кому стоїть, поставити можна. А якщо ще вся логіка всередині, то простіше новий написать, ніж в цьому розбиратися.

Звичайно ж, тут теж it depends, і в декомпозуванні теж має бути розумна межа, але бога в серці треба мать, і не робити конячі функції з компонентами.

Згоден з тим, що як Ви сказали — it depends. Як я й підкреслював у статті — якщо ми відштовхуємось від архітектури, то може бути ситуація, коли компонент великий, але він уже складається з атомів, є цілісною фічею і зрозумілий у читанні. І тоді 400 рядків — це абсолютно нормально.

Питання ж не у «магічних цифрах», а в тому, наскільки швидко нова людина може зрозуміти, що відбувається. Якщо це забезпечено — розбивати далі немає сенсу.

Або Ви вважаєте, що незалежно від зрозумілості все ж існує умовна межа кількості рядків, після якої треба виносити щось окремо?

Часом, один компонент на 400 рядків читається краще за 2 компоненти по 50 рядків + 5 кастомних хуків з кількома рівнями вкладенності.

Locality of behavior робить свій вклад в читабельність, як не крути.

Підписатись на коментарі