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

Борг фронтенду як витрати на зміни: діагностика зростаючої бази коду React

Дізнайтеся, як розпізнавати складні рішення, які роблять великі додатки на React, Redux, TypeScript та Next.js дорогими для змін, та як поступово погашати цей борг.

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

Джерела: Frontend Technical Debt: The Problems Nobody Notices Until the React App Becomes Huge

Великі додатки на React майже ніколи не стають складними для підтримки через один катастрофічний коміт. Вони погіршуються внаслідок довгої серії невеликих, на перший погляд розумних рішень: додатковий useEffect, фрагмент стану Redux, який не потребував бути глобальним, „тимчасовий“ компонент з 500 рядків, спільний віджет, який постійно отримує нові параметри, файл utils, який збирає все те, що не має очевидного місця розташування, типи API, скопійовані до трьох модулів, правила стилізації, до яких ніхто не наважується повертатися. Жодне з цих рішень не здається неправильним у момент його прийняття. Проблеми починаються тоді, коли сотні таких рішень накопичуються та починають взаємодіяти між собою.

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

Як чистий репозиторій поступово псується

Новий проект зазвичай починається зі структури, яку кожен може одразу прочитати:

src/
  components/
  pages/
  hooks/
  services/
  store/

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

src/
  components/
    Button/
    Modal/
    Table/
    UserTable/
    UserTableNew/
    CommonTable/
    GenericTable/
    SharedTable/
    ...
  hooks/
    useUser.ts
    useUsers.ts
    useUserData.ts
    useFetchUser.ts
    useUserDetails.ts
    ...
  utils/
    helpers.ts
    common.ts
    misc.ts
    formatter.ts
    newHelpers.ts
    oldHelpers.ts
  store/
    user/
    users/
    userData/
    userProfile/
    ...

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

Швидкий шлях, який потай стає постійним

Найпоширенішою причиною такого „боргу“ є відкладена оптимізація. Функціонал має бути випущений сьогодні; ретельна реалізація зайняла б два дні, а швидка — три години. Вибір версії, яка вимагає три години, не обов’язково означає погану інженерну практику — іноді це саме правильне бізнес-рішення.

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

„Давайте просто додамо це до існуючого компонента.“ „Ми переробимо це після випуску.“ „Поки що залишимо це в Redux.“ „Ми зможемо узагальнити його пізніше.“ „Більше ніхто не буде цього потребувати.“

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

Коли один компонент відповідає за всю функціональність

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

function Checkout() {
  // fetch products
  // validate coupon
  // calculate totals
  // manage address
  // handle payment
  // track analytics
  // show modals
  // handle errors
  // render UI
}

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

  • взаємодію з API
  • бізнес-правила та перевірку даних
  • керування станом
  • аналітику та перевірку дозволів
  • навігацію та обробку помилок
  • стилізацію та відображення

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

Багаторазові компоненти, які коштують більше, ніж економлять

На початку кар’єри багаторазовість здається безумовною перевагою. З досвідом стає зрозуміло, що кожна абстракція має свою ціну. Розгляньмо таблицю, яку зробили „налаштовуваною“:

<DataTable
  data={data}
  columns={columns}
  loading={loading}
  selectable
  searchable
  sortable
  pagination
  serverPagination
  expandable
  virtualized
  stickyHeader
  renderEmptyState={...}
  renderLoadingState={...}
  onRowClick={...}
/>

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

Кориснішим тестом, ніж «чи можна це використовувати повторно?», є питання про те, чи полегшить абстракція створення наступної функціональності. Компонент із тридцятьма пропами часто зовсім не є архітектурою, придатною для повторного використання; це кілька окремих компонентів, об’єднаних під одним API. Композиція, коли користувачі складають менші елементи, часто є кращим вихідом, як описано у способах усунення перевантаження пропами в React за допомогою композиції та слотів.

Де папка utils приховує ваш домен

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

utils/
  formatDate.ts
  formatCurrency.ts
  userHelpers.ts
  permissions.ts
  productHelpers.ts
  validation.ts
  apiHelpers.ts
  stringUtils.ts
  common.ts
  helpers.ts

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

Краще запитання полягає не в тому, "у який файл з інструментів це має потрапити?", а в тому, "яка бізнес-функція відповідає за цю логіку?" Орієнтація на функції робить відповідь зрозумілою:

features/
  checkout/
    components/
    hooks/
    services/
    types/
    utils/

products/
    components/
    hooks/
    services/
    types/
payments/
    components/
    hooks/
    services/
    types/

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

Redux як універсальний сховище даних

Redux — це справді корисний інструмент, але розміщення всього в глобальному стані — це один із найшвидших способів створення „боргу“ у фронтенді. Сховище зазвичай починається з частин, які однозначно підходять для глобального використання:

auth
user
permissions
cart
products
checkout
notifications

Потім воно поглинає елементи, які насправді стосуються лише одного екрана чи однієї взаємодії:

modal
filters
selectedTab
searchText
formState
temporaryUIState

З часом цей магазин починає функціонувати як база даних для всього, що дві компоненти можуть захотіти прочитати. Технічно майже будь-яке значення може потрапити до Redux, тож питання „Чи можливо це?“ є неправильним. Правильне питання — чи справді стан потребує бути глобальним. Розподіл стану на три категорії допомагає.

Стан UI, який має залишатися локальним

Такі значення зазвичай мають знаходитися поруч із компонентом, який їх використовує, у useState або useReducer:

isModalOpen
activeTab
inputValue
hoveredItem
expandedRow

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

Стан сервера, який потребує стратегії кешування

Дані, якими володіє сервер, мають інші вимоги:

products
orders
users
invoices

Тут серйозними проблемами є кешування, усунення дублікатів, повторне отримання даних та їх анулювання. Це вимагає свідомого підходу до отримання даних (спеціалізованої бібліотеки для керування станом сервера або еквівалентної схеми), а не власноруч написаних функцій-редукторів, які копіюють відповіді до сховища. Варіанти компромісів детальніше розглядаються у статті про переосмислення стану сервера у великих додатках за допомогою React Query та Redux.

Стан, який справді є загальним для всього додатку

Деякі проблеми справді стосуються всього продукту:

authentication
permissions
application-wide preferences
cross-feature workflows

Це розумні кандидати на роль глобального сховища. Урок полягає не у тому, щоб „уникати Redux“, а у тому, що сама лише доступність не є підставою для роботи з глобальним станом.

Борги TypeScript: типи, які втрачають своє значення

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

const data: any = response.data;

Кожен елемент any вимикає перевірку саме у момент, коли дані потрапляють до додатку, тому компілятор не може попередити вас про зміни структури API. Ще однією поширеною проблемою є окреме оголошення одного й того ж типу в кількох файлах:

type User = {
  id: string;
  name: string;
}

Якщо це поширити на кілька команд, вийде сукупність майже ідентичних рішень:

User
UserType
UserModel
UserResponse
UserDTO
UserData

Розробники потім витрачають більше зусиль на вирішення, який тип імпортувати, ніж отримують користь від системи типів. Прагнення до ситуації, коли «у всього є тип», не відповідає суті. Мета полягає у тому, щоб важливі елементи системи — такі як відповіді API, об’єкти домену та інтерфейси компонентів — були чітко визначені та описані лише один раз. Хороший TypeScript ускладнює представлення недійсних станів; поганий TypeScript лише створює більше файлів. Корисним підходом є використання транспортного типу для даних, які повертає API, та доменного типу для даних, з якими працює інтерфейс користувача, з їх конвертацією в одному місці, замість шести нечітко пов’язаних варіантів.

Коли useEffect стає стандартним вихідним рішенням

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

useEffect(() => {
  // do something
}, [something]);

Потім другий ефект підтримує синхронність двох значень:

useEffect(() => {
  // synchronize something
}, [a, b]);

Третій варіант існує переважно для корекції поведінки попереднього:

useEffect(() => {
  // fix previous effect
}, [data]);

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

Типовим прикладом є зберігання похідних даних у стані та синхронізація їх з ефектом:

const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Це спочатку відображає стару порожню рядок, потім виконує ефект, а потім знову відображає. Цю значення можна просто обчислити під час відображення:

const fullName = `${firstName} ${lastName}`;

Це здається дрібною різницею, але в масштабах всього додатку такі незначні рішення сумуються. Функції типу Effects призначені для синхронізації з системами поза React, такими як підписки, таймери чи DOM, а не для отримання одного стану з іншого. Більш детальний огляд цього теми можна знайти у статті про припинення синхронізації стану з useEffect.

Межа сервер/клієнт у Next.js

Сучасний Next.js додає новий вимір до архітектурних рішень. Окрім визначення того, який компонент відображає певний контент, необхідно також вирішити, де буде виконуватися код: на сервері, на клієнті чи в обох місцях. Ця межа сама по собі є архітектурним рішенням.

Частою помилкою є перетворення великої частини структури проекту на клієнтські компоненти лише через потребу одного невеликого елемента в інтерактивності. Це рішення слід вказати на початку файлу сторінки:

"use client";

І тепер вся сторінка надсилається до браузера у вигляді клієнтського JavaScript:

export default function ProductPage() {
  // entire page becomes client-side
}

Усе це через те, що одній кнопці потрібен фрагмент локального стану:

const [isOpen, setIsOpen] = useState(false);

Це зручно, але така зручність в одному місці має наслідки в іншому: більше JavaScript надсилається до браузера, а отримання даних, яке могло б залишитися на сервері, тепер мусить відбуватися на клієнті. У термінах App Router усе, що імпортується з файлу "use client", стає частиною клієнтського пакету, тож розташування директиви впливає набагато більше, ніж лише на один компонент, який ви хотіли зробити інтерактивним.

Корисно мислити по шарах, відкладаючи інтерактивність якомога нижче в ієрархії:

Server
   ↓
Data fetching
   ↓
Feature boundary
   ↓
Interactive client component
   ↓
Small local state

замість простого спрощеного підходу:

Everything → Client Component

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

Борг у стилізації — це все одно борг

Команди часто сприймають технічний борг як проблему JavaScript та TypeScript. Стилізація так само накопичує борг, особливо у продуктах з тривалим життєвим циклом, які поєднують кілька підходів одночасно: CSS-in-JS та styled-components, CSS-модулі, класи для стилізації, старі глобальні таблиці стилів та кілька систем дизайну. Рано чи пізно бібліотека компонентів міститиме щось на кшталт цього:

Button
ButtonV2
PrimaryButton
NewButton
StyledButton
CommonButton
LegacyButton

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

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

Коли дублювання краще за абстракцію

Цей урок часто найважче прийняти досвідченим розробникам. Припустимо, існують два компоненти, які наразі виглядають приблизно на 90% схожими:

ComponentA
ComponentB

Інстинктивно хочеться об’єднати їх у один:

<UniversalComponent />

Проте іноді дублювання є дешевшим варіантом, оскільки ви ще не знаєте, чи будуть обидва елементи розвиватися в одному напрямку. Якщо вони розійдуться, об’єднаний компонент наповниться умовними операторами, і кожна зміна в одному з елементів може пошкодити інший. Невелике дублювання є корисним; передчасна абстракція може створити зв’язок, який триватиме роками.

Практичний стандарт: напишіть код двічі, вивчіть закономірності, і абстрагуйте лише тоді, коли третій випадок використання підтвердить потребу в цьому. Це не універсальний закон, але це розумне початкове правило.

Залежності, які більше не мають підстав існувати

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

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

    package.json
    

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

    Справжня ціна: повільніші та ризикованіші зміни

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

    Feature request
          ↓
    Small change
          ↓
    Tests
          ↓
    Deploy
    

    У додатку з великою кількістю залежностей той самий запит перетворюється на складне розслідування:

    Feature request
          ↓
    Find where this behavior lives
          ↓
    Understand 5-year-old code
          ↓
    Check Redux dependencies
          ↓
    Check shared component
          ↓
    Check side effects
          ↓
    Check styling overrides
          ↓
    Change code
          ↓
    Something else breaks
          ↓
    Fix regression
          ↓
    Add another workaround
    

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

    Чому фраза „все все ще працює“ є оманливим сигналом

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

    „Чи працює додаток?“ — це тому слабкий спосіб перевірки стану. Набагато кориснішим запитанням, особливо коли системи ростуть, є: наскільки дорого коштує безпечно змінити цей додаток?

    Скорочення боргу без повної переробки

    Спокусливою відповіддю на проблемний код є повна перебудова. Типовий результат через шість місяців є передбачуваним:

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

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

    Визначення дорогих сегментів

    Не піддавайтеся спокусі переписувати все заново. Натомість визначте ті частини продукту, з якими розробники постійно мають проблеми, наприклад:

    Checkout
    User Management
    Search
    Dashboard
    Reporting
    

    Для кожного кандидата шукайте такі ознаки:

    • Баги, які постійно з’являються в одній і тій самій області
    • Файли, які люди бояться редагувати
    • Компоненти, які зустрічаються майже в кожному pull request
    • Частини продукту, які новачки опановують найдовше
    • Функції, зміни в яких впливають на несуміжні модулі

    Області, які постійно з’являються, є вашими «точками високого боргу».

    Вимірюйте сьогоднішню вартість змін

    Не потрібні складні інструменти. Достатньо відстежувати кілька показників:

    Average time to implement feature
    Average PR size
    Number of files touched
    Regression frequency
    Build time
    Test execution time
    Deployment frequency
    

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

    Визначте правила залежностей перед вибором паттерну

    Перш ніж обговорювати Clean Architecture чи Feature-Sliced Design, вирішіть більш базове питання: що може залежати від чого? Простий напрямок розташування компонентів може виглядати так:

    app
     ↓
    features
     ↓
    shared
    

    У межах цього підходу функціональність може залежати від спільних елементів будови:

    features/product
          ↓
    shared/ui
    shared/lib
    

    але спільний код ніколи не повинен впливати на функціональність:

    shared/ui
          X
    features/product
    

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

    Лейаут, орієнтований на функціональності, для React та Next.js

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

    src/
      app/
        routes/
        providers/
        layouts/
    
    
    features/
        checkout/
          components/
          hooks/
          services/
          state/
          types/
        products/
          components/
          hooks/
          services/
          state/
          types/
        users/
          components/
          hooks/
          services/
          state/
          types/
      shared/
        ui/
        hooks/
        lib/
        types/
    

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

    Звички, яких слід уникати у великому фронтенді

    Під час приєднання до великого кодового базису React або його підтримки будьте обережні щодо наступного:

    • Глобалізація стану за замовчуванням. Більшості значень ніколи не потрібно залишати свої компоненти.
    • Занадто раннє узагальнення. Спочатку зрозумійте варіації, а потім узагальнюйте.
  • Додавання ефекту як рефлексу. Ретельно обміркуйте потік даних перед синхронізацією чого-небудь.
  • Відкриття іншого файлу utils.ts. Знайдіть домен, який має володіти логікою.
  • Позначення цілих сторінок як клієнтського коду. У App Router це особливо дороговартісно.
  • Встановлення пакету для того, що вже робить платформа чи фреймворк. Кожен такий пакет стає довгостроковим обов’язком з технічного обслуговування.
  • Рефакторинг перед вимірюванням. Чистота коду — це не бізнес-результат; нижча вартість змін — так.
  • Найважливішою зміною, яка виникає з досвідом, є зміна цілей, а не поява нових технік. Спочатку природним питанням є те, як краще написати певний фрагмент коду. Пізніше питання змінюється на те, як залишити систему простішою для того, хто буде її змінювати далі. Така переоцінка змінює підхід до компонентів та абстракцій, керування станом за допомогою Redux, TypeScript, Next.js, стилізації, тестів, загальної архітектури та залежностей. Замість вдосконалення окремих файлів починається оптимізація здатності системи до еволюції.

    Борг — це інструмент, а не ворог

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

    Тож ціль „нульового боргу“ є неправильною. Реалістичною метою є такий рівень боргу, який команда розуміє, може контролювати та поступово погашати, коли він починає впливати на процес розробки.

    Що насправді ускладнює роботу з великим додатком

    Варто бути точним щодо первинних причин, адже це рідко ті фактори, за які звинувачують люди:

    • Проблемою часто не є окремі компоненти; проблемою є взаємозв’язки між ними, які ніхто не може зрозуміти.
  • Наявність Redux — це не проблема; проблема в тому, що ніхто не знає, що має там бути.
  • Велика кількість типів TypeScript — це не проблема; проблемою є типи, які більше не відображають значущих меж.
  • Наявність серверних та клієнтських компонентів у Next.js — це не проблема; проблемою є команда, яка не знає, де проходить межа.
  • Старий код — це не проблема; проблемою є старі рішення, які більше не можна безпечно змінити.
  • Коротко кажучи, технічний борг — це не код, який виглядає потворно. Це код, який робить зміни непотрібно дорогими, а найкраща архітектура фронтенду — це не та, яка виглядає ідеально з першого дня, а та, яка все ще дозволяє команді швидко працювати через п’ять років.

    Чек-лист перед додаванням функціоналу

    Перш ніж додавати новий функціонал до кодової бази React, розгляньте ці запитання:

    • Чи є справжня причина для того, щоб цей стан був глобальним?
    • Чи належать ці дані бекенду, чи це просто стан інтерфейсу?
    • Чи бере на себе компонент другу чи третю відповідальність?
    • Чи вирішує нова абстракція певну потребу, чи просто робить код більш охайним?
    • Яка функція є власником цього бізнес-правила?
    • Чи може цей код працювати на сервері в Next.js замість цього?
    • Чи справді тут необхідний Effect?
    • Чи доведеться підтримувати ще один пакет?
    • Чи справді компонент використовується у різних функціях?
    • Чи може новачок зрозуміти цей код, не запитуючи того, хто його написав?

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

    Теми, які варто дослідити далі

    Кілька розділів цього посібника заслуговують на більш детальне розглядання окремо:

    • Коли Redux дійсно є правильним вибором порівняно з локальним станом, Context, Redux Toolkit, RTK Query, спеціалізованим обробленням серверного стану, станом URL та станом форм.
    • Рефакторинг компонента з 1 000 рядків без порушення функціонування продакшну у вигляді поетапного кейс-стаді.
    • Чи погіршують переосмислені компоненти якість додатку, з урахуванням проблеми „вибуху“ параметрів, композиції та систем дизайну.
    • Серверні та клієнтські компоненти в Next.js, з акцентом на рішення щодо меж функціоналу, а не на те, що робить "use client".
    • TypeScript у масштабах, включаючи any, генеріки, DTO, типи домену та API, дублювання та інференцію.
    • Що має замінити папку utils як місце для логіки.
  • Знання моменту, коли додатку потрібна рефакторинг архітектури, ідеально за допомогою моделі оцінки, побудованої на таких критеріях:
  • Change cost
    Bug frequency
    Dependency coupling
    Component size
    State complexity
    Build time
    Testability
    Developer onboarding
    

    Ключові висновки

    • „Борг“ фронтенду — це сума дрібних, розумних рішень; стежте за тенденцією, а не за окремими змінами.
    • Оцінюйте стан кодової бази за вартістю та ризиком змін, а не за тим, чи працює додаток наразі.
    • За замовчуванням тримайте стан локальним, сприймайте дані сервера як проблему кешування та глобалізуйте лише те, що справді є спільним для всього додатку.
    • Віддавайте перевагу межам та напрямку залежностей перед назвами папок чи названими методологіями.
    • Поступово погашайте „борг“ у визначених проблемних місцях, замість того щоб ризикувати повною переробкою.