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

Разбор слишком крупных компонентов React без ухудшения производительности в продакшене

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

Издатель: REACTAPP.TOP17 мин чтения3880 слов

Источники: React Components Are Getting Too Big: How to Refactor Without Breaking Production

В каждой зрелой кодовой базе на React есть такой компонент: он функционирует корректно, приносит доход и вызывает страх у всех, кто вынужден им пользоваться. Изначально это был простой экран, но затем к нему добавились фильтрация, пагинация, вызовы API, система разрешений, проверка данных, диалоговые окна, возможности экспорта, аналитика и всё больше различных эффектов. В этом руководстве объясняется, как уменьшить размер такого компонента путём снижения его сложности, а не только размера файла, и как делать это поэтапно, не ставя под угрозу его работу в производственных условиях.

Типичный пример постепенно включает в себя все аспекты данной функции:

function ProductManagement() {
  // API calls

  // Redux selectors
  // Local state
  // Filters
  // Pagination
  // Sorting
  // Selection
  // Permissions
  // Validation
  // Business rules
  // Export
  // Modal handling
  // Analytics
  // Hundreds of lines of JSX
}

Когда количество строк превышает тысячу, кажется, что очевидной проблемой является длина кода. Но это не так. Длинный компонент сам по себе не является проблемой; проблемой становится компонент, в котором много факторов, требующих изменений. Описанные ниже действия не связаны с разделением одного файла на десять, а направлены на снижение затрат и рисков при последующих изменениях.

Количество строк — неверный показатель

Правила вроде «переписать всё, что содержит более 300 строк», хотя и легко применять, плохо помогают выявить реальные риски. Представьте файл из 500 строк, состоящий в основном из декларативных тегов с минимальным количеством логики. Теперь представьте файл из 200 строк, в котором смешаны вызовы API, переходы состояний, проверки прав, валидация, эффекты, бизнес-правила и вложенная условная отрисовка. Файл с меньшим количеством строк почти наверняка сложнее в безопасных изменениях.

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

Симптомы того, что сложность стала препятствием

Обращайте внимание на эти признаки:

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

    Определите ответственности перед тем, как изменять код

    Первым шагом не является извлечение JSX в редакторе. Это запись того, что на самом деле делает компонент. Для экрана управления продуктом структура инвентаря может выглядеть так:

    ProductManagement
    │
    ├── Fetch products
    ├── Transform API response
    ├── Filter products
    ├── Sort products
    ├── Manage selection
    ├── Validate permissions
    ├── Export data
    ├── Open/close dialogs
    ├── Handle errors
    └── Render UI
    

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

                  ProductManagement
                         │
           ┌─────────────┼─────────────┐
           │             │             │
           ▼             ▼             ▼
         Data         Behavior      Presentation
           │             │             │
           ▼             ▼             ▼
         API       Filters/Rules      Table
         State     Selection          Dialog
                   Validation         Actions
    

    Именно с такой группировки начинается качественная рефакторинговая работа. Сначала необходимо определить границы, а уже затем — файлы.

    Цель: слои с четким распределением обязанностей

    Разделение одного большого файла на пять меньших меняет его структуру на диске, но не дизайн:

    BigComponent.tsx
          ↓
    5 smaller .tsx files
    

    Настоящая цель — создание многоуровневой структуры, в которой интерфейс пользователя, поведение, логика домена и доступ к данным имеют свои определенные места:

    ProductPage
         │
         ├─────────────── UI
         │
         ├─────────────── Hooks / Behavior
         │
         ├─────────────── Domain Logic
         │
         └─────────────── Data Access
    

    На практике использование папок, ориентированных на функции, — удобный способ этого достичь:

    features/products/
    │
    ├── components/
    │   ├── ProductFilters.tsx
    │   ├── ProductTable.tsx
    │   ├── ProductActions.tsx
    │   └── ProductDialog.tsx
    │
    ├── hooks/
    │   ├── useProducts.ts
    │   ├── useProductFilters.ts
    │   └── useProductSelection.ts
    │
    ├── services/
    │   └── productService.ts
    │
    ├── utils/
    │   ├── productMapper.ts
    │   └── productValidator.ts
    │
    └── types.ts
    

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

    • ProductTable отвечает за отображение таблицы.
    • useProductFilters хранит логику фильтрации.
    • useProducts предоставляет данные о продуктах React.
    • productService взаимодействует с бэкендом.
    • productValidator содержит правила проверки данных.

    Желаемая структура — однонаправленный поток, при котором каждый уровень зависит только от уровня ниже него:

    UI
     ↓
    Behavior
     ↓
    Data Access
     ↓
    Backend
    

    а не сеть, в которой каждая проблема влияет на все остальные:

    UI
     ↕
    State
     ↕
    API
     ↕
    Business Logic
     ↕
    UI
    

    Чтобы узнать больше о проблеме перегрузки свойств в React, посмотрите статью о сокращении перегрузки свойств и «богатых» компонентов в React.

    Шаг первый: вынести чистую логику из React

    Чистые функции представляют собой способ извлечения кода с наименьшим риском, поэтому они являются естественной отправной точкой. Помощники для работы со статусом обычно определяются внутри компонента:

    const getProductStatus = (product: Product) => {
      if (!product.active) {
        return "Inactive";
      }
    
      if (product.stock <= 0) {
        return "Out of Stock";
      }
      return "Available";
    };
    

    Ничто в нем не зависит от React: нет useState, нет useEffect, нет Redux, нет контекста. Нет причин, по которым компонент должен хранить его у себя, поэтому переместите его в отдельный модуль:

    utils/productStatus.ts
    

    Как только он окажется там, правило можно будет протестировать напрямую, без отрисовки чего-либо:

    describe("getProductStatus", () => {
      it("returns inactive for disabled products", () => {
        expect(
          getProductStatus({
            active: false,
            stock: 10
          })
        ).toBe("Inactive");
      });
    });
    

    Это значительная выгода при минимальном риске: бизнес-правило теперь отделено от процесса отображения, а сбой в тесте указывает непосредственно на само правило, а не на страницу. Руководящий принцип прост: если какая-то логика не требует React, React не должен отвечать за неё.

    Второй шаг: выделение поведения React в хуки

    Далее идут те элементы, которые действительно являются частью React: состояние и эффекты, работающие вместе. Загрузка продуктов — типичный пример, состоящий из трех переменных состояния и одного эффекта:

    const [products, setProducts] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    
    useEffect(() => {
      setLoading(true);
      fetchProducts()
        .then(setProducts)
        .catch(setError)
        .finally(() => setLoading(false));
    }, []);
    

    После того как код полностью перенесён в пользовательский хук, он превращается в единый элемент с небольшим значением возвращения:

    function useProducts() {
      const [products, setProducts] = useState([]);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        setLoading(true);
        fetchProducts()
          .then(setProducts)
          .catch(setError)
          .finally(() => setLoading(false));
      }, []);
      return {
        products,
        loading,
        error
      };
    }
    

    Затем экран использует его в одной строке:

    function ProductManagement() {
      const {
        products,
        loading,
        error
      } = useProducts();
    
    // composition
    }
    

    Теперь компонент содержит меньше деталей реализации и больше похож на описание того, из чего состоит экран. Перенос кода буквально сделан намеренно: извлечение не должно изменять поведение. Улучшения, такие как установка значения loading в true изначально (чтобы при первой отрисовке экран не казался временно «загруженным»), игнорирование ответов после демонтирования компонента или использование библиотек для управления состоянием сервера, должны быть реализованы в отдельных последующих изменениях.

    Не всё должно превращаться в хук

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

    useProductName()
    useProductStatus()
    useProductLabel()
    useProductColor()
    useProductFormatter()
    

    Это не является архитектурным улучшением. Пользовательский хук имеет смысл тогда, когда он оборачивает поведение React: состояние, эффекты, контекст, другие хуки, подписки или аспекты жизненного цикла. Если вам нужен лишь форматтер,

    formatProductName(product)
    

    В таком случае простой вызов функции — идеальное решение:

    formatProductName(product)
    

    Если обернуть его в хук,

    useProductName(product)
    

    это приводит к соблюдению правил использования хуков, запрещает их вызов при определенных условиях или внутри циклов, но не приносит никакой пользы. Абстракции должны снижать умственную нагрузку на кодовую базу, а не увеличивать её.

    Определите, кто отвечает за каждый элемент состояния

    Местоположение состояния — одно из самых важных решений при рефакторинге. Полезная классификация делит данные сервера, глобальное состояние интерфейса и локальное состояние интерфейса:

    Application State
    │
    ├── Server State
    │   ├── Products
    │   ├── Orders
    │   └── Customers
    │
    ├── Global UI State
    │   ├── Authentication
    │   ├── Theme
    │   └── Application Preferences
    │
    └── Local UI State
        ├── Modal
        ├── Selected Tab
        └── Temporary Form State
    

    Ни один из крайних вариантов не работает: сохранение всего в Redux приводит к образованию объемного глобального хранилища, а сохранение всего в локальной памяти вызывает дублирование и сложные цепочки передачи данных. Для каждого элемента состояния необходимо ответить на следующие вопросы:

    • Какие компоненты его читают?
    • Как долго он должен существовать?
    • Он принадлежит серверу или клиенту?
    • Должен ли он сохраняться при навигации?
  • Существует ли уже источник истины для этого?
  • Надежный эвристический принцип: храните состояние как можно ближе к его потребителям, но не ближе, чем позволяют их общие потребности.

    Избегайте создания «верховного родителя»

    Разделение компонента может привести к новой проблеме. Дочерние компоненты становятся меньше по размеру, но родительский компонент теперь хранит всё состояние и передает его дальше:

    <ProductPage
      products={products}
      filters={filters}
      setFilters={setFilters}
      selected={selected}
      setSelected={setSelected}
      permissions={permissions}
      loading={loading}
      error={error}
      onExport={handleExport}
      onDelete={handleDelete}
      onEdit={handleEdit}
    />
    

    Файл стал короче, но уровень взаимосвязи остался прежним; он просто переместился на уровень выше. Прежде чем обращаться к контексту или глобальному хранилищу, чтобы скрыть эти параметры, спросите себя, действительно ли состояние является общим или же границы компонентов определены неверно. Контекст уместен, когда несколько потомков действительно нуждаются в одном и том же значении, а глобальное хранилище — когда состоянием владеет вся приложение. Ни то, ни другое не должно маскировать границы, которые не соответствуют структуре приложения.

    Постепенная замена компонента с использованием шаблона Strangler Fig

    Переписывание всего компонента сразу — самый рискованный вариант. Замена его по частям гораздо безопаснее. В начале старый компонент выполняет всю работу:

    BigComponent
    └── Everything
    

    Затем одна из функций, например фильтрация, переходит в отдельный компонент, в то время как остальное остается нетронутым:

    BigComponent
    ├── ProductFilters
    └── Everything else
    

    На следующем этапе обрабатывается таблица:

    BigComponent
    ├── ProductFilters
    ├── ProductTable
    └── Everything else
    

    В конечном итоге страница становится полностью составной, а устаревшая реализация исчезает:

    ProductPage
    ├── ProductFilters
    ├── ProductTable
    ├── ProductActions
    └── ProductDialog
    

    Это шаблон Strangler Fig, обычно описываемый для миграции бэкенд-систем, применяемый к фронтенду: новые элементы постепенно формируются вокруг старой реализации, каждый из них берет на себя одну функцию, в то время как остальная часть приложения продолжает работать. Преимущество заключается в том, что в производственной среде не требуется делать резкий переход

    100% Legacy
    

    от прямо к

    100% New
    

    в одной версии продукта. Каждое промежуточное состояние может быть выпущено и восстановлено.

    Сохраняйте стабильность публичного контракта

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

    <ProductManagement
      clientId={clientId}
      onSelect={handleSelect}
    />
    

    Сохраняйте этот интерфейс неизменным на протяжении всего рефакторинга. За его пределами вы можете полностью изменить структуру:

    Existing Public API
            │
            ▼
     ProductManagement
            │
       ┌────┼────┐
       ▼    ▼    ▼
     Hooks  UI  Services
    

    Пользователи никогда не должны знать, что внутренние механизмы переместились в хуки, компоненты интерфейса и сервисы, что снижает влияние каждого изменения. Эта дисциплина особенно важна для общих библиотек компонентов, систем дизайна, микро-фронтендов, корпоративных приложений и всего того, что используется несколькими командами. Если API действительно требует изменений, делайте это в виде отдельной, явной миграции.

    Разделяйте эффекты, скрывающие множество обязанностей

    Большие компоненты часто содержат эффекты, которые выполняют несколько независимых действий:

    useEffect(() => {
      fetchData();
    
      updateAnalytics();
      validatePermissions();
      calculateSomething();
    }, [dependency]);
    

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

    useEffect(() => {
      fetchData();
    }, [dependency]);
    
    useEffect(() => {
      updateAnalytics();
    }, [analyticsDependency]);
    

    Когда уже разбираетесь с этим, обратите внимание на эффекты, которые лишь вычисляют значения. Хранение отфильтрованного списка в состоянии и синхронизация его через эффект приводят к дополнительной отрисовке и возникновению устаревших данных:

    const [filteredProducts, setFilteredProducts] = useState([]);
    
    useEffect(() => {
      setFilteredProducts(
        products.filter(product => product.active)
      );
    }, [products]);
    

    Вычисление его во время отрисовки проще и всегда даёт единообразный результат:

    const filteredProducts = products.filter(
      product => product.active
    );
    

    Храните в кэше или синхронизируйте данные только тогда, когда есть веская причина, например, дорогостоящее вычисление (в таких случаях инструментом является useMemo, а не эффект).

    Разделяйте серверное состояние от состояния интерфейса

    На более крупных экранах различие между этими двумя категориями быстро окупается:

                         ProductPage
                            │
                ┌───────────┴───────────┐
                │                       │
           Server State             UI State
                │                       │
          ┌─────┴─────┐          ┌──────┴──────┐
          │           │          │             │
       products    loading     filter        modal
       error       status      selected      tab
    

    Список продуктов и наличие открытого диалогового окна представляют собой совершенно разные типы данных. Бэкенд отвечает за продукты, а браузер — за диалоговые окна. Данные сервера требуют кэширования, повторного загрузчения, обработки ошибок, тогда как состояние интерфейса является временным и локальным. Смешивание их в одном объекте useState затрудняет понимание компонентов. Как это реализуется с использованием специальных инструментов описано в React Query и Redux: переосмысление состояния сервера в крупных приложениях.

    Процесс рефакторинга, а не полная переработка

    Рефакторинг в производственной среде представляет собой цикл:

    Identify
       ↓
    Understand
       ↓
    Protect
       ↓
    Extract
       ↓
    Test
       ↓
    Validate
       ↓
    Deploy
       ↓
    Measure
       ↓
    Repeat
    

    Это не так:

    Delete component
          ↓
    Rewrite everything
          ↓
    Hope production works
    

    Именно это различие отличает рефакторинг действующей системы от запуска совершенно нового проекта.

    Как выглядит результативный поток данных

    При разделении ответственностей взаимодействие пользователя проходит через четко определенные слои и возвращается обратно:

    User
     │
     ▼
    ProductPage
     │
     ▼
    useProductFilters
     │
     ▼
    useProducts
     │
     ▼
    ProductService
     │
     ▼
    API
     │
     ▼
    Backend
     │
     ▼
    ProductService
     │
     ▼
    useProducts
     │
     ▼
    ProductTable
     │
     ▼
    Updated UI
    

    В сокращенном виде принцип представляет собой прямую линию зависимостей:

    Presentation
          ↓
    Behavior
          ↓
    Data Access
          ↓
    Backend
    

    Таблица не должна знать, как работает аутентификация, сервис API не должен знать, как отображается таблица, а бизнес-правило не должно интересоваться тем, используется ли в интерфейсе модальное окно или выдвижной элемент. Это и есть разделение ответственностей с конкретной целью: любые изменения остаются внутри одного слоя.

    Следите за дублированием запросов к API

    Такая регрессия встречается настолько часто, что заслуживает отдельного предупреждения. До рефакторинга один компонент загружал данные:

    ProductPage
         │
         └── fetchProducts()
    

    После выделения как страница, так и ее дочерний компонент вызывают один и тот же хук для получения данных:

    ProductPage
       │
       ├── useProducts()
       │
       └── ProductTable
              │
              └── useProducts()
    

    Код выглядит более аккуратным, но теперь простой хук fetch-in-effect выполняется дважды, что удваивает количество запросов и может привести к отображению разных версий данных в компонентах. Каждый раз, когда вы меняете логику получения данных, спросите, у кого находится контроль над состоянием сервера. Должен существовать единый источник правды. Если несколько компонентов нуждаются в одних и тех же удаленных данных, следует получить их один раз и передать дальше или использовать слой кэширования состояния сервера, который устранит дублирование запросов, вместо того чтобы позволять каждому потребителю загружать данные независимо.

    Тесты — это защитный механизм

    Перед изменением критически важного компонента перечислите поведения, от которых зависит бизнес:

    Product Management
    │
    ├── Filter products
    ├── Select products
    ├── Edit product
    ├── Delete product
    ├── Export products
    └── Permission handling
    

    Затем защитите их на соответствующем уровне:

    • Единичные тесты подходят для проверки валидации, преобразований, бизнес-правил, форматирования и сопоставления данных.
    • Тесты компонентов подходят для проверки отображения, взаимодействия, а также состояний загрузки и ошибок.
  • Тесты «конец-конец» подходят для критически важных рабочих процессов, поведения, охватывающего несколько компонентов, и интеграции с бэкендом.
  • Для рефакторинга не требуется 100%-ная охватность тестами. Необходимо иметь уверенность в поведении тех элементов, которые действительно важны. Самыми ценными являются тесты, написанные с учетом текущего поведения до начала работ, поскольку они подтверждают, что ничего не изменилось.

    Небольшие pull-запросы — часть дизайна

    Затраты на проверку и возврат к предыдущему состоянию растут по мере увеличения объема изменений. Сравните такие варианты:

    Extract ProductFilters
    

    за которым следует

    Extract ProductTable
    

    а затем

    Extract product-fetching behavior
    

    и одно изменение с названием

    Refactor ProductManagement
    

    которое затрагивает

    4,000 lines changed
    

    Небольшие изменения проще рассматривать и откатывать, они сокращают риски возникновения регрессий, сохраняют читаемую историю изменений в Git, упрощают отладку и снижают количество конфликтов при слиянии. Для технического лидера это скорее способ помочь команде работать параллельно, чем просто предпочтение в отношении качества кода.

    Разделяйте рефакторинг от других изменений

    Не стоит объединять рефакторинг с всеми остальными пунктами списка желаемых изменений:

    React upgrade
          +
    State migration
          +
    API changes
          +
    UI redesign
          +
    CSS migration
          +
    Component refactoring
    

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

    Refactor
       ↓
    Stabilize
       ↓
    Measure
       ↓
    Optimize
    

    Одно значимое изменение за раз позволяет отслеживать каждую возможную регрессию.

    Флаги функций для замен с высоким риском

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

    return useNewProductExperience
      ? <NewProductManagement />
      : <LegacyProductManagement />;
    

    Затем вы можете постепенно увеличивать степень экспозиции, отслеживая при этом показатели ошибок и бизнес-метрики:

    1%
     ↓
    5%
     ↓
    25%
     ↓
    50%
     ↓
    100%
    

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

    До и после

    Исходная точка, в сокращённом виде, объединяет в одной функции обработку данных, фильтрацию, выбор, экспорт и диалоговые окна:

    function ProductManagement() {
      const [products, setProducts] = useState([]);
      const [filter, setFilter] = useState("");
      const [selected, setSelected] = useState([]);
      const [loading, setLoading] = useState(false);
      const [showDialog, setShowDialog] = useState(false);
    
      useEffect(() => {
        // Fetch products
      }, []);
      const filteredProducts = products.filter(...);
      const handleSelect = (...) => {
        // Selection logic
      };
      const handleExport = (...) => {
        // Export logic
      };
      return (
        <>
          {/* hundreds of lines of JSX */}
        </>
      );
    }
    

    В этом нет ничего ошибочного. Проблема в том, что один компонент отвечает за слишком много функций. После рефакторинга страница связывает хуки с компонентами отображения:

    function ProductManagement() {
      const {
        products,
        loading
      } = useProducts();
    
    const {
        filter,
        setFilter,
        filteredProducts
      } = useProductFilters(products);
      const {
        selected,
        handleSelect
      } = useProductSelection();
      return (
        <ProductPage>
          <ProductFilters
            value={filter}
            onChange={setFilter}
          />
          <ProductTable
            products={filteredProducts}
            selected={selected}
            onSelect={handleSelect}
            loading={loading}
          />
          <ProductActions
            products={selected}
          />
        </ProductPage>
      );
    }
    

    Теперь на странице описывается принцип композиции, который является наилучшей формой взаимодействия между компонентами на уровне страницы. Каждый хук и дочерний компонент можно читать, тестировать и изменять отдельно.

    Меньше не значит быстрее

    Хочется предположить, что преобразование

    1 large component
    

    в

    10 smaller components
    

    улучшает производительность. Однако это не всегда так, и это может ухудшить ситуацию из-за дополнительных отрисовок, повторных вычислений, шумных обновлений контекста, дублирующихся запросов, ненужного использования useMemo и useCallback, а также свойств, меняющих свою идентичность при каждой отрисовке. Архитектура и производительность связаны, но представляют собой отдельные аспекты. Сначала измеряйте, затем оптимизируйте, и используйте инструменты профилирования и телеметрию в режиме работы для подтверждения того, действительно ли что-то улучшилось.

    Оценивайте успех по стоимости следующих изменений

    Уменьшение количества строк — это приятный побочный эффект:

    1,200 lines → 300 lines
    

    Но это не является конечной целью. Сравните компонент до и после рефакторинга по таким показателям:

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

    Цели, к которым вы стремитесь:

    Complexity       ↓
    Coupling         ↓
    Regression risk  ↓
    Testability      ↑
    Maintainability  ↑
    Developer speed  ↑
    

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

    Когда не следует рефакторить

    Не каждый крупный компонент заслуживает таких усилий. К плохим причинам относятся: большой размер файла, предпочтение к маленьким файлам, мода на новую архитектуру, желание внедрить другую библиотеку или несоответствие кода личным вкусам. Если компонент стабилен, хорошо протестирован, редко изменяется, с низким уровнем риска и изолирован, то масштабная переработка может принести больше рисков, чем устранит. Решение сводится к компромиссу:

    Expected future benefit
              │
              ▼
       Is it worth the
        refactoring risk?
              │
           Yes / No
    

    Это инженерное решение, основанное на ожидаемых будущих изменениях, а не предпочтение в форматировании.

    Самая сложная часть — понимание существующего поведения

    Написание новой реализации редко бывает самой сложной частью. Сложность заключается в том, чтобы достаточно хорошо понимать текущее поведение компонента, чтобы сохранить то, на чем зависят пользователи. В продакшен-компонентах содержится знание, которое невозможно понять просто из их кода: неописанные предположения по продукту, правила доступа, связи с более старыми системами, необычные ответы сервера, которые они терпят, события отслеживания, на которых зависят другие компоненты, обходные решения для определенных браузеров, вызывающие компоненты, о которых никто не помнит, патчи от прошлых инцидентов и ожидания других команд. Именно это скрытое знание делает полную переписку гораздо более рискованной, чем поэтапную декомпозицию. Порядок приоритетов, который следует иметь в виду: сначала сохранить поведение, затем улучшить структуру, и только потом оптимизировать.

    Чек-лист для рефакторинга в продакшене

    Перед началом:

    • Найдите всех потребителей данного компонента.
    • Задокументируйте его текущее поведение и определите его функции.
  • Определите, кто владеет каждым элементом состояния и от каких API он зависит.
  • Выделите критически важные рабочие процессы, проанализируйте существующие тесты и зафиксируйте базовые показатели производительности в продакшене.
  • Во время рефакторинга:

    • Сохраняйте стабильность публичного интерфейса.
    • Сначала извлекайте чистые функции, затем логику работы React.
    • Размещайте состояние рядом с теми компонентами, которые им пользуются, и избегайте абстракций, которые не приносят пользы.
    • Исключайте нерелевантные изменения, следите за дублирующимися запросами и делайте патчи небольшими.

    Перед слиянием:

    • Проверки типов, тесты на единицу, компоненты и полный цикл, а также процесс сборки должны пройти успешно.
    • Не должно быть дублирующихся запросов и никаких неожиданных сбоев в отображении.

    После развертывания:

    • Отслеживайте ошибки, объем трафика API и производительность, сравнивая их с базовыми показателями.
    • Следите за критически важными рабочими процессами пользователей.
    • Удалите устаревший код, как только новый путь подтвердит свою надежность.

    Заключение

    Большие компоненты React — не враг; врагом является неконтролируемая сложность. Неверная стратегия выглядит следующим образом:

    Big Component
          ↓
    Rewrite Everything
    

    Правильная стратегия — терпеливый цикл, ведущий к более простой архитектуре:

    Big Component
          ↓
    Understand
          ↓
    Protect
          ↓
    Extract
          ↓
    Test
          ↓
    Measure
          ↓
    Repeat
          ↓
    Simpler Architecture
    

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