Галоўная / Артыкулы / Калі повторна адаптация компанентаў React пагубна: выбух пропаў і спосаб яго адліквідацыі

Калі повторна адаптация компанентаў React пагубна: выбух пропаў і спосаб яго адліквідацыі

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

2023 слоў

Спяшаныя компаненты павінны заўсёды эканоміць час, але ў багацых кодбазах фронтэнду існуе адна компанента, яю всі бояцца правяць: <Modal /> або <Card /> з дзесяткамі параметраў, дзе вылечыць бяг на аднам экране пашкодзіла трохі іншых. Такі результат рэдкая разліква недбаласці; гэта стандартны наследак занадто раннего пераўтварэння UI. У гэтым артыкуле показана, як компанента стае такім трыбутам, пояснюецца, чаму дуплікацыя UI часта ёсць дышэўнейшым выборам, і прадстаўлены два практычныя інструмента для ўтримання ад гэтага: складныя компаненты і правіло трох.

Як безнэпакойны скораць становіцца компанентам з трыдзяцьма двума параметрамі

Зазвычай такі патэрны начынаюцца з разумнага запиту. Дизайнер аддае экран практыкавання з дыялогам, який па большай частцы падобны дыялогу пאўтварэння на сторанцы налашоўкаў. Разлікі незначныя: адзінкі розташаваны з левага боку, пад заглавлам є апранкі з колерам апельсына, а тонкая лінія раздзеляе кнопкі і контэнт.

Пад час адзорты каму-то ўсклікваецца, што вялікая часть <Modal /> вялікае часть вже існуе, і пропануецца ўжоўтарыць яе. Чэрез калькі гадзін прыходзіць запрос пра змяны, які дадае чатыры параметры: hasOrangeBadge, alignActionsLeft, showDividerLine і badgeText.

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

Чаму прымета DRY дзейнае інакш у UI

«Не павтараце сябе» — гэта розумны адказ з прычыной. Калі бізнес-логіка, такая як вычысленне платежа або перакананне ў правах, дублюецца, налагоджэнне ў адной копіі заставляе іншыя застацца зламанымі, а копіі пачынаюць розходзіцца.

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

Два элементы, якія выглядаюць аднародна, не павінны быць тым самым паняйом. Злучэнне двух шаблонаў UI ў адну спільную компаненту пры тым, калі ўжо не выкрэслены ўсе деталі ўзора, стварае зв’язак межу функцыямі, якога раней не існавало. З таго часу незначныя змены ў дизайне для процеса адкалікавання можу вымагаць паўтарнай перапраналізы налашоўкаў, системы рахункування і аналітыкі, проста таму, што гэтыя элементы адображаюцца як адна і тая ж компанента. У такі момент павторны выкарыстоўвання стае нашкодным.

Анатамія «эксплазіі» пропаў

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

interface CardProps {
  title: string;
  description: string;
  onClick?: () => void;
}

Адбудова таксама мінімальная і простая для чытання:

export function Card({ title, description, onClick }: CardProps) {
  return (
    <div className="card" onClick={onClick}>
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  );
}

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

interface CardProps {
  // ...
  imageUrl?: string;
  imageAlt?: string;
}

У спрінт 7 каманда дашборду просіць кнопку дзеяння ў канцы, видную толькі на актыўных элементах. Для цього патрэбны флаг, айконка і обробнік:

interface CardProps {
  // ...
  hasTopRightAction?: boolean;
  topRightActionIcon?: React.ReactNode;
  onTopRightAction?: () => void;
}

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

interface CardProps {
  // ...
  subtitle?: string;
  badgeText?: string;
  badgeVariant?: "success" | "warning" | "danger";
  isExpandable?: boolean;
  expandedContent?: React.ReactNode;
  defaultExpanded?: boolean;
}

Да спрінту 20 функцыя атрыбутавання стала сукупнасцю умоў. Корэнтны элемент вылічае свой клас на адварцы, а зображэнне атрыбутуецца толькі тады, калі ёсць URL:

export function Card(props: CardProps) {
  return (
    <div
      className={`card ${
        props.isExpandable ? "card-expandable" : ""
      }`}
    >
      {props.imageUrl && (
        <img src={props.imageUrl} alt={props.imageAlt} />
      )}

Контэйнер заголовка ачынаецца з назвай:

      <div className="card-header">
        <div>
          <h3>{props.title}</h3>

праз падзагалоўкі, якая паказваецца толькі якщо ёна заданая:

          {props.subtitle && <h4>{props.subtitle}</h4>}
        </div>

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

        {props.hasTopRightAction && (
          <button onClick={props.onTopRightAction}>
            {props.topRightActionIcon}
          </button>
        )}
      </div>

Бэдж створае назву класа на адварцы свайго варіянта і моўчкі пераходзіць на default варіянт, які нават не перыядначаны ў типе:

      {props.badgeText && (
        <span
          className={`badge badge-${
            props.badgeVariant || "default"
          }`}
        >
          {props.badgeText}
        </span>
      )}

Апісанне, едынай часткі, якая засталася з первіснага дизайна, распакоўваецца посередзіне:

      <p>{props.description}</p>

І розширюваныя ніжкі заканчваюць компонент. Заўважыце, што isExpandable керуе як корневым класам, так і ніжкамі, тады як defaultExpanded з інтэрфейса нігде не выкарыстоўваецца ў маркапе:

      {props.isExpandable && (
        <div className="card-footer">
          {props.expandedContent}
        </div>
      )}
    </div>
  );
}

Такія маленькія несувязанасці ўзнакомыя. Калі у компоненте столькі флагоў, ніхто не можа адразу бачыць усе комбінацыі, і з’яўляюцца некоректныя або часткова реалізаваныя статусы. Кожны корыстувальнік <Card /> должен выучыць все большы список опцый і з’ясаваць, якія комбінацыі падтрымляюцца. Абстракцыя тепер стае важкай для разумэння, чым просты маркап, які ёй было запланавана заменіць.

Дуплікацыя часта дешэўей, чым некоректная абстракцыя

Вядомая фраза з дзялекі разрабаткі програмнага апарату, папулярная завяць Санді Мэц, проста і ясна: "Дуплікацыя ў дзесяткі разоў дашчэцей, чым некоректная абстракцыя." Работа з фронтэндам — гэта тая сфера, дзе гэтыя парады прыносяць наібольшы рэзультат.

Калі два компоненты проста супадаюць адзін з іншым, ў большасці случаў безпечнейшы выбор — трываліць іх адналежна. Падазроўваючы, што BillingModal і OnboardingModal ўсё жа є незалежнымі компонентамі:

  • Змены ў BillingModal не можа паўлічыць на OnboardingModal.
  • Адменшэнне функцыі аднаўлення выключае яго модальное вікно і всю логіку, прызначаную для гэтай функцыі.
  • Кожны модальны элемент развіваецца па сабе, адпаведна своім трэбам.
  • Незначны візуальны змяненні не выклікае патрэбы разумець дзесяткі параметраў, якія належаць іншым функцыям.

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

Складныя компоненты: кампанізацыя замест на конфігурацыю

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

Ёсць дыялог для вырачэння рахунку, створаны такім чынам. Компонент-власнік зберагае стан «відкрыты» локальна:

export function BillingSettings() {
  const [isOpen, setIsOpen] = useState(false);

Корневы <Modal> прымеў толькі тое, што ён практычна володзіць — стан «відкрыты» і функцыю-калебак, а надпіс ёсць самастоятельной часткай:

  return (
    <Modal open={isOpen} onOpenChange={setIsOpen}>
      <Modal.Overlay />

У зоне контэнту ёсць загалоўкі, а ў загалоўцы — тэлеграфна назва:

      <Modal.Content>
        <Modal.Header>
          <Modal.Title>Update Billing Plan</Modal.Title>

Індыкатор ёсць проста іншым элементам-дзецюком загалоўкі, прычому його варіянт выражаецца як атрыбут самога індыкатора:

          <Modal.Badge variant="warning">
            Action Required
          </Modal.Badge>
        </Modal.Header>

Тэла ўтримвае будь-які контэнт, які пачынаецца з прыемленае сообщэння:

        <Modal.Body>
          <p>
            Please update your payment method to avoid account suspension.
          </p>

і продовжваецца формай, спецыфічной для паведамлення, пра якую сам модальны акна нічога не ведае:

          <CreditCardForm />
        </Modal.Body>

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

        <Modal.Footer align="right">
          <Button
            variant="ghost"
            onClick={() => setIsOpen(false)}
          >
            Cancel
          </Button>

Основная дзеянне завершае ніжку і всю структуру:

          <Button variant="primary">
            Save Changes
          </Button>
        </Modal.Footer>
      </Modal.Content>
    </Modal>
  );
}

Структурныя адміністрацыі маюць значэнне. Калі модальныя вікна патрабуюць індыкатора, не існуе прызначэння showBadge, якое можна было б дадзіць; тады вы выводзіце <Modal.Badge />. Калі экрану патрабуецца спецыяльны контэнт, яго размешчаюць там, дзе ён паслужыць, замест таго, каб ствараць новы флаг. Прычынны эфекты ўсунуцца без додатковых зусиль:

  • Няма зайвых прызначэнняў. Модальна вікно без індыкатора або ніжней часткі проста не будзе выводзіць <Modal.Badge /> чы <Modal.Footer />.
  • Большая гнучкасць. Айконка пад заголовкам размешчаецца ўнутрь <Modal.Header>; не патрабуецца новага прызначэння.
  • Ізольаваная стылізацыя. Змена <Modal.Badge /> не павінна вплываць на основны контейнер.
  • Чытальная структура. JSX паказвае распалоць безпосередзя, замест таго каб чытальнікам было трэба ачыць інтерфейс TypeScript і выявляць, якае сумешанне пропаў дае такі распалоць.
  • Пад спадом, складныя компаненты зазвычай дзеліцца станам, такім як open, через React context, і самэ гэта дазволяе <Modal.Footer> або кнопцы закрыцья даўаць да яго доступ без неабходнасці праходжання праз роўнень пропаў. Композіцыя перадае кантроль корыстніку, не прыводзячы компаненту ў формат об’екта налаштавання. Гэты падход не ўсёвылік: система дизайна павінна задокументаваць, якія часткі могу быць вмешчаныя там, дзе, а корыстнікі прабываюць пісаць трохі больш маркапу з кожным выкарыстоўваннем. Чытайце пра іншы падход да такой рефакторкі ў нашай аднарозе па выправленню перанасыця пропаў за дапамогою композіцыі і слотаў.

    Правіло трох для выдзельвання спакойных компанентаў

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

    Першы выклікання: запісуйце яго на месца

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

    Другі выклікання: копіюйце і прыстосавайце

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

    Трэці выклікання: выделяйце з доказамі

    Калі трэці, адзінаковы экран патрабуе той самы візуальны і поведэнчны шаблон, у вас нарэшце ёсць достатнек падтверджэнняў, каб зрозумець, што насправды ўзаўмна і шта разлічыцца. Такое дзейсцеўнае чаканне адкрывае справжнія незменныя элементы — тыя часткі, якія застаюцца тымі ж у кожны раз, і справжнія варіяцыі, якія павінны застацца гнучкімі. Гэтыя варіяцыі ўзгодныя як кандыдаты на викорыстанне ў структурах складання, а не як булевыя атрыбуты.

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

    Ключовыя выводы

    • Старацеся утримваць колькасць атрыбутаў пад контролем. Калі компонент продовжае атрыбутавацца додатковымі налашчаннямі для некалякіх сцэнарыяў викорыстоўвання, перад тым, як дадаць ўсё новы атрыбут, пераканайцеся ў правільнасці абстракцыі.
    • Вярніцеся да структураў складання замест налашчанняў. Дзеціны, слоты і складныя компоненты забезпечваюць гнучкасць без неабходнасці стварання новых булевых атрыбутаў ў кожны раз, калі змінюецца дизайн.
  • Прыняце некалькі прыкладаў дуплікацыі. Пара маленькіх, адзінаковых компанентаў, якія дзелюцца частымі элементамі маркаупу, часта лёгкавей кераваць, чым адзін компанент, які прагне пакрыць усе варыянты.
  • Застосавайце правілу трох. Дазвольце, каб спяльныя компаненты выйшлі з кальколькох рэальных прыкладаў выкарыстоўвання, а не з прыкладоў прагнозаў.
  • Хорашая архітектура фронтэнду не мерыцца колькасцю лінакоў коду, а тым, насколькі безпечна можа змяніцца код без паслядков для всей аплікацыі. Інодзе найкращы компанент — не той, які викорыстоўваецца всюды, а той, які застаёцца сам.

    Спадневаная літэратура

  • Дзе палягае межа фрэймворку ў стаке UI, які можна викорыстоўваць знову — Дазвольце вам дазнацца, як машыны станаў, Web Components, а таксама лейаут і рух, базаваныя на атрыбутах, дапамагаюць забезпечыць працэсу UI жыццё пасля заканчэння жыцця фрэймворку, і калі такая переноснасць не ўзраджвае.
  • Дуплікацыя проты сувязі: як вяршыць, калі трэба ствараць спяльны код — Дазвольце вам дазнацца, чаму злічыненне сэрагодных компонентаў React або служб NestJS можа коштаць больш, чым простая дуплікацыя, і як выкарыстоўваць тры пытанні, каб вяршыць, кали абстракцыя справды заслуговае на свое месца.
  • Дизайн з фічамі, раздзеленымі на часткі, для React: шары, правілы імпорту і календар зупніцься — Дазвольце дазнацца, як шары дизайну з фічамі, раздзеленымі на часткі, правілы однонаправленага імпорту, простыя публічныя API і @x крос-імпорты дапамагаюць расплутаць кодбазы React, а таксама календар зупніцься на ўжыцце толькі часткі цых методаў.