Галоўная / Артыкулы

Разбір занадто великіх компонентаў 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, якія выкананы безпосередна з компонентаў UI
  • бізнес-правілы, якія знаходзяцца ўнутры обробчыкаў клікаў
  • глыбока ўвёрнутая умовная візуалізацыя
  • логіка дазволаў, якая перасылаецца разам з прэzentацыяй
  • тая ж трансформацыя дадзеных, якая павтараецца ў калькі месцаў
  • разныя абавязкі па чытанні і запісе таго ж стану
  • дзьвянгі ланцюжкі пропаў, якія перадаюцца чераз проміжныя компоненты
  • дуплікаваныя запыткі сеціі
  • тэсты, якія павінны запусціць цэлую сторанку, каб пераканацца ў адной маленькай правіле
  • Частыя канфлікты з’еднання ў той самы файл
  • Найболей яўным сімптамам є не тэхнічныя, а соцыяльныя проблемы: людзі ухілляюцца ад работы з гэтым файлам. Калі фраза „Не чапайце за гэта, ўсё равано ўсё зламаецца“ становіцца частым выразам у командзе, компонент перестае быць проста пытаннем стылю і стае інжынерным рызыкам.

    Апрацаваць адповідальнасці перш чым працаваць з кодам

    Першы крок — гэта не выкалікаванне 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, у статыцы «Cutting React prop drilling and god components down to size».

    Першы крок: вынесць чыстую логіку з 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)
    

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

    Адначыце, хто ўладае кожным элементам стану

    Месца, дзе знаходзіцься стан, — адна з найважлівейшых рашэнняў падчас рефакторавання. Корыстная класыфікацыя разлічва данні сервера, глобальны стан UI і локальны стан UI:

    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

    Перапісваўце цэлага компонента за адну раз ёсць найбольш рызыкаваны варыянт. Замена яго частка за частка ўсё болей безпечная. Спачатку стары компонент выканае всё:

    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
    

    Корыстувачы ніколі не патрабуюць ведаць, што внутрашняя структура перайшла у форму хуків, компонентаў UI і служб, што зменшывае адзінаковасць наследкавання кожныя змены. Гэта правіло ўсё болюць важлівае для спаканаў спяльных компонентоў, систем дизайна, мікро-фронтэндаў, корпоратывных прыемоў і ўсьго таго, што викорыстоўваецца колькімая командамі. Якщо 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 є інструментам, а не эфектам).

    Раздзеліце стан сервера ад стану UI

    У большых экранах розрыв гэтых двух категорыяў дае быстрыя паследкі:

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

    Співвідношэнне спісу продуктав і стану адкрытага дыялога ўоснове являе сабо разныя виды дадзеных. Бэкэнд керуе продуктамі; браузер — дыялогам. Дадзеныя сервера патрабуюць кэшавання, падтрымкі пацягвання, завантажэння і обробкі адзінакоў, тады калі стан UI є трымчасовым і локальным. Аднаўленне ўсіх гэтых элементаў у аднам об’екте 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% пакрыцтва. Неабходна толькі вярасц у тое, якое поведанне мае значэння. Тэсты, напісаныя на адповідачнае нынешняму поведанню, якія вы выпрацавалі перад пачаткам, є найцэнныяй, таму што яны падтверджаюць, што нічога не змянілася.

    Маленькіе запросы прыўязкі ўскладненняў є часткаю дызайна

    Косцт разгляду та косцт вярнення да пачатковага стану зростаюць разам з розмерам змян. Поручыце такія прыклады:

    Extract ProductFilters
    

    праз

    Extract ProductTable
    

    і потым

    Extract product-fetching behavior
    

    з адной змянай пад назвай

    Refactor ProductManagement
    

    якая вплывае

    4,000 lines changed
    

    Маленькі PR-запросы лёгкаяя да адзначыць і вярнуць, зменшаюць рызык регрэсій, залучаюць чытальную історыю 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.
    • Размішчаць стан падалей ад тых, хто яго выкарыстоўвае, і адмовіцца ад абстракцый, якія не прыносяць корысты.
    • Не включаць неканэксуальныя змены, старацца утримваць колькість запытаў адносова малой і утримваць PR-ы невялікімі.

    Перад з’еднанням:

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

    Пасля развяртання:

    • Стежыць за памылкамі, трафікам API і выкарыстоўваннем ресурсаў, а таксама порываць іх з базовымі показнікамі.
    • Старацца стежыць за критычнымі процесамі работы корыстнікаў.
    • Адзін раз пасля таго, як новы шлях з’явіцца надзеям, трэба адчыніць застарелы код.

    Заключанне

    Большыя компоненты React не ўсё; ворам являецца неконтрольаваная складнасць. Поражальная стратэгія выглядае так:

    Big Component
          ↓
    Rewrite Everything
    

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

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

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