Главная / Статьи

Долг фронтенда как стоимость изменений: диагностика растущей базы кода 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, поэтому вопрос «Можно ли это?» неверен. Правильный вопрос — действительно ли состояние должно быть глобальным. Разделение состояния на три категории помогает в этом.

Состояние интерфейса, которое должно оставаться локальным

Подобные значения обычно должны находиться рядом с компонентом, который их использует, в 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}`;

Кажется, что это незначительная разница, но в масштабах всего приложения такие мелкие решения накапливаются. Функции useEffect предназначены для синхронизации с системами вне 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
    

    Каждый шаг в этой второй цепочке — это время, потраченное на понимание проблемы вместо её решения, причем последние два шага особенно показательны: изменение ломает что-то не связанное, а исправление требует ещё одного временного решения. Именно это и есть «проценты по кодовой базе», а также то, что на самом деле представляет собой технический долг.

    Почему фраза «всё ещё работает» — вводящий в заблуждение сигнал

    Приложение может работать без ошибок, имея огромный технический долг. Страницы загружаются, пользователи входят, заказы обрабатываются, панель управления отображается, а индикатор CI находится в зелёном состоянии, поэтому все считают, что код в порядке. Затем появляется новая функция, и, казалось бы, простое изменение занимает две недели. Именно в этот момент долг становится заметным.

    «Работает ли приложение?» — это довольно слабая проверка его работоспособности. Гораздо более полезный вопрос, особенно по мере роста систем, звучит так: насколько дорого безопасно изменить это приложение?

    Сокращение долга без полной переработки

    Соблазнительным решением проблем с беспорядочной структурой кода кажется его полная переработка. Однако типичный результат через шесть месяцев предсказуем:

    • старое приложение по-прежнему работает в производстве
    • замена выполнена лишь частично
    • теперь требуется техническое обслуживание двух систем
    • команда истощена
    • работа над продуктом застопорилась

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

    Определение дорогостоящих участков

    Сопротивляйтесь желанию полностью переписать код. Вместо этого выделите те части продукта, с которыми разработчики постоянно сталкиваются с трудностями, например:

    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 и состоянием форм.
    • Рефакторинг компонента из 1000 строк без нарушения работы приложения в виде пошагового кейс-стади.
    • Способствуют ли переиспользуемые компоненты ухудшению качества приложения, включая проблему «взрыва свойств», концепции композиции и систем дизайна.
    • Серверные и клиентские компоненты в Next.js, с акцентом на решения, связанные с границами их использования, а не на функции метода "use client".
    • TypeScript в крупных проектах, включая использование типов any, генериков, DTO, типов домена и API, а также проблемы дублирования и автоматического вывода типов.
    • Что может заменить папку utils в качестве хранилища логики.
  • Понимание момента, когда приложению требуется рефакторинг архитектуры, желательно с использованием модели оценки, основанной на таких критериях:
  • Change cost
    Bug frequency
    Dependency coupling
    Component size
    State complexity
    Build time
    Testability
    Developer onboarding
    

    Основные выводы

    • «Долг» фронтенда — это сумма небольших, разумных решений; следите за тенденциями, а не за отдельными изменениями.
    • Оценивайте состояние кодовой базы по стоимости и риску изменений, а не по тому, работает ли приложение в настоящее время.
    • По умолчанию храните состояние локально, рассматривайте серверные данные как проблему кэширования и глобализируйте только то, что действительно кросс-функционально.
    • Вместо названий папок или определенных методологий предпочитайте четкие границы и направление зависимостей.
    • Постепенно сокращайте «долг», сосредотачиваясь на наиболее проблемных участках, а не рискуя полной переработкой.