Выкарыстоўванне композіцыі і слотаў для адлагоджэння перанасыць пропамі ў React
Дазнайце, чаму React-праблікі з вялікаю канфігурацыёй ствараюць проблемы з адтрыманам, і як інверсія кантролю, композыцыя та слоты дапамагаюць ствараць справжная перадуваецца компаненты.
Крыніцы: Your React Components Aren’t Reusable. They’re Just Complicated
Разгляд таго, як параметры, заданыя налашчэнням, паціхамо ствараюць проблемы з адтрыманнем, і як інверсія кантролю разам з композыцыёй прыводзяць да справжная модульных компанентаў UI.
Уявіце запрос ад колегі пра дапамогу, у яком ён з гордасцю называе ствараны элемент "Універсальная табліца дадзэнняў."
Мета — адразу задовольніць усі патрэбныя для атрыбутавання табліц у всем кодбэсе.
У яго ўключаецца трыдзяць чатыры окремыя параметры:
<UniversalDataTable
data={users}
columns={columns}
enablePagination={true}
paginationType="cursor"
enableSorting={true}
showFilterRow={true}
hasExportToCsvButton={true}
exportButtonText="Download CSV"
headerBackground="light-gray"
denseRows={false}
enableRowSelection={true}
selectionMode="multiple"
renderCustomRowActions={(row) => <DropdownMenu row={row} />}
onRowClick={(row) => navigate(`/users/${row.id}`)}
/>
Пад капотам знаходзіцца прыбліжна 1 200 ліній логіки.
Для атрыбутавання спісу корыстнікаў ён працюе чыста.
Чэрез калькі недзеў маркетынг прасіць стварыць табліцу для порэвання цэнаў, якая мае быць на публічным сайте.
Трэбаванні: тры столбцы, іконкі з галочкамі пад кожным рядкам функцый, і выдзеленыя рамкі пад назвай "Популярныя" адносна середньага столбца.
Каліхтанець працуе з <UniversalDataTable />, спакульваючы, што ён проста будзе работаць.
Самэй тады все ламаецца.
Няма можлівасці наскальковаць грані адзінаковых клетак.
Выравняванне заголовка зафіксаванае ў адзінам конкрэтным лейяуте flex.
Ёсць патрэба выключыць кнопку экспорту у формате CSV, тады трэба паслаць hasExportToCsvButton={false} плюс калькі іншых параметраў, толькі каб пасягнуць чысты рэзультат.
Таму каліхтанець дадае ўсё больш параметраў:
isPricingTable
popularColumnIndex
customHeaderRenderer
disableGlobalStyles
Файл вырастае да 1,450 ліній.
У такі момент яго вяліка частка не можа быць перызнаемым.
Тое, што спачатку было абстракцыяй, ператворылася на клубок спецыяльных прыкладоў, якія адагаюцца, што ёсць аднам компонентам.
1. Пастка параметраў канфігурацыі
Калі каліхтанець хоча, каб компонент быў "перызнаемым," інстынктыўны выбар зазвычай — канфігурацыя чераз параметры.
Кожны новы случай выкарыстоўвання спрыяе дадаць ў інтэрфейс пропаў ўжо адной зменнай, перыяду стрынгаў, функцыі-вызову або змены стылю.
Тыпавы прыклад:
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
hasLeftIcon?: boolean;
leftIcon?: React.ReactNode;
hasRightIcon?: boolean;
rightIcon?: React.ReactNode;
isFullWidth?: boolean;
tooltipText?: string;
tooltipPosition?: 'top' | 'bottom';
rounded?: boolean;
}
На першы погляд гэта здаецца прыемлемым.
Але проблема тая, што гнучкасць, базаваная на канфігурацыі, мае цену, якую не можна адразу пазначыць.
1. Комбінатывны выбух
Ужо дзесяць булевых пропаў тэорычна даўаюць 1 024 можлівыя комбінацыі.
Не трэба теставаць ўсе іх, каб зрозумець, як быстра ситуацыя выйходзіць з-пад контролю.
Дзеяныя комбінацыі суперасункуюцься межаў.
Іншыя проста застануцца невыкарыстанымі назаўсёды.
Дзеяныя будуць адрасавацца неоднакова залежна ад шырыны вікна перагляду, атмосфернага кантэнту чы ўглублення компонента.
З часам у вас застаецца купа экстремальных случаяў замест адносна цяпловага, прыемлемага API.
2. Проніканне ў пропы
Утварыце, што настройка падказкі должна дася да вкладанага кэшу, які знаходзится на калькі слоёў усередине дрэва компанентаў.
Кожны проміжны компанент на шляху тепер должен перадаваць проп, які ў яго на самай працэ не выкарыстоўваецца.
Канфігурацыя праходзіць чераз слоі выключна таму, што так трэба структуре, а не таму, што гэта мае сэнс там.
Гэты ўласна механізм працы проп-дрілінгу.
3. Жорсткія прыпускі ў лейауте
Сюды зазвычай абарачваюцца компаненты, якія керуюцца канфігурацыяй.
Падазроўваю, што новы дизайн выкарыстоўвае іконку, якая ставіцца между двумя рядкамі тэксту кнопак.
Да дадання чагосьці падобнага:
iconPosition="between"
можна на час падправіць ситуацыю.
Але як толькі дизайн зноў змяніцца, што тады?
Чырза калькі часу вам даведзецца падтрымваць цэлы набор варіянтаў:
iconPosition="left"
iconPosition="right"
iconPosition="top"
iconPosition="between"
iconPosition="absolute"
Співлект працюе ўсё больш, таму што внутрэній JSX-код компаненту яшчэ абсалютна керуе його макетам.
Настоямыя проблемы не ў колькасці наяўных пропаў.
Проблема ў тым, што компанент бере на сябе занадта многія адпаведнасці за рэшты, якія не ў яго компетенцыі.
2. Інверсія кантролю: лепейшы ментальны модэль
Прыгожая можлівасць павторнага выкарыстоўвання не досягаецца праз спробы заздалегідь прагназаваць кожную будучую патрэбу.
Ёй спрыяе застосаванне інверсіі кантролю (IoC).
Канцэпцыя простая:
Уместа таго, каб компанент керуў усім, што выводзится ў яго межах, нехта, хто яго выкарыстоўвае, керуе тымі часткамі, якія трэба зменіць.
Самэ гэтая прычына, чаму композіцыя ў React являе сабою такі моцны патэрн.
У зменшэнне колькасці элементаў, якія трэба адразу падтрымліваць — падказкі, іконкі, элементы з анімацыяй загрузкі, значкі і ўсе можлівыя варыянты макета, — стоіць стварыць кантактную пляшчу, чыя галоўная функцыя — проста быць кантактной пляшчу.
Напрыклад:
export function Button({
children,
variant = 'primary',
className = '',
...props
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} ${className}`}
{...props}
>
{children}
</button>
);
}
Контроль над зместам тепер належыць корыстніку:
<Tooltip content="Save your profile">
<Button>
<SaveIcon />
<span>Save Changes</span>
</Button>
</Tooltip>
Зверніце ўвагу, што больш не ёсьць неабходнасці.
Кантактная пляшча не мае жадных ведамасяў пра існаванне падказак.
Няма атрыбута hasLeftIcon.
Няма атрыбута leftIcon.
Няма атрыбута iconSpacing.
Няма атрыбута hasSpinner.
Патрэбны элемент з анімацыяй загрузкі? Проста разместіце яго безпасова ўнутрь кантактной пляшчы.
<Button>
<Spinner />
Saving...
</Button>
Компанент стае меньшым за розмерам, але здобывае большую гнучкасць.
Гэты компроміс — галоўная ідея ў гэтым падходзе.
Компанент, які можна викорыстоўваць па-разныя разы, не патрабуе внутршняго адмаўлення кожнага можлівага сцэнарыю. Яму патрабуе чыста апісаная межа, пры якой іншыя компаненты можна складваць.
3. Слоты спрыяюць прыемнейшам расширэнню складных компанентов
Складванне з children добра працуе для простых элементаў. Але што будзе з болей складнымі элементамі, такімі як карточкі, дыялогавыя вікна, панелі керування чыста панелі прафіля?
Самэльце тут і становяць свою ценнасць слоты і складныя компаненты.
Уявіце сабе стандартную карточку прафіля, побудаваную на наборе атрыбутаў:
<UserProfileCard
avatarUrl="/avatar.jpg"
name="Sarah Connor"
role="Tech Lead"
status="Active"
bio="Building reliable frontend systems."
primaryActionLabel="Send Message"
onPrimaryAction={sendMessage}
secondaryActionLabel="View Profile"
onSecondaryAction={viewProfile}
badgeColor="green"
/>
На першы погляд, гэта здаецца зручным.
Потым трэбаванні начынаюць зменяцца.
Команда з маркетынгу хочаць, каб адрыс у сябрэннях у соцыяльных меражах быў паказаны проста ля імені чалавека. Команда адміністрацыі для корпатыўных аккаунтаў патрабуе, каб банер з напісам „Suspended“ быў растягнуты ўжо на всю шырыну карточкі. Іншая команда, якая займаецца продуктамі, хочаць трохі кнопак дзеяння замест двух. Дизайнер вырашае, што аватар павінен стаяць справа, а не злева.
Кожны з эых запитоў ператвараецца ў ўжо адну з додатковых вимог.
Лепшы падход — паказаць структуру карточкі як складовы елементы:
<Card>
<Card.Header>
<Avatar
src={user.avatarUrl}
alt={user.name}
/>
<div>
<Card.Title>{user.name}</Card.Title>
<Card.Subtitle>{user.role}</Card.Subtitle>
</div> <Badge variant={user.statusColor}>
{user.status}
</Badge>
</Card.Header> <Card.Body>
<p>{user.bio}</p>
</Card.Body> <Card.Actions>
<Button
variant="secondary"
onClick={viewProfile}
>
View Profile
</Button> <Button
variant="primary"
onClick={sendMessage}
>
Send Message
</Button>
</Card.Actions>
</Card>
Тепер вы можаце бачыць структуру карточкі ў JSX. А раз вы яе бачыце, вы можете яе зменіць.
Хочаце ўсё ж адну большую кнопку дзеяння? Дадайце яе. Патрабуеце іншы значак? Заменіце яго. Павінны дадаць ўсё ж адной больш банер? Увесьце яго там, дзе ёму і адпаведае. Хочаце зовсім іншаг распадзелу загалоўка? Проста напісце новую загалоўку.
Нічага з гэтаго не выклікае патрэбы касацца надмёрна большага спільнай складовай, каб падтрымаць адзін новы варіант.
4. Композіцыя лепша за налашчэнне
Этыя два падходы ставяць абсалютна разныя запитанні.
Налашчэнне ставяць запитанне:
"Калькі варыянтав можа выкліквати гэта складовая?"
Композіцыя ставяць запитанне:
"Калькі маленьких элементаў можу корыстувачы з’еднаць, каб створыць тое, чаго ў іх насправдзе трэба?"
Налашчэнне продовжае пакладаць большую відпаведальнасць за прыняття рашэнняў на саму складовую. Композіцыя вертае гэтыя рашэнні таму, хто яе выкарыстоўвае. Зазвычай гэта болей правільнае месца для такой відпаведальнасці.
Вазьміце гэта паўстанавленне як прыклад:
<Button
hasLeftIcon
leftIcon={<SaveIcon />}
hasTooltip
tooltipText="Save your profile"
showSpinner={isSaving}
spinnerPosition="left"
/>
протыва:
<Tooltip content="Save your profile">
<Button>
{isSaving && <Spinner />}
<SaveIcon />
Save Profile
</Button>
</Tooltip>
У другім фрагменте, звяжана, болей JSX. Але ў яму таксама значна меншая залежнасць між часткамі, які не павінны знаць пра іншыя. Такі компроміс зазвычай ўартуе — незначны дадатковы маркап на месцы викліку, як правило, ў далекай перспективе є дешэвейшым, чым дадаць ўштое постаяннае функцыяналізацыю да спакульнага компонента.
5. Навучыцеся у бібліятэках без галоўкі
Некалькі савэцкіх бібліятэкаў фронтенду выключна добра прадстаўляюць гэтую філасофію.
Праекты, такія як Radix UI, TanStack Table і shadcn/ui, сыгралі важную ролю у папулярныцы гэтага падходу, які ставіць композыцыю на першае месца.
TanStack Table — адны з найяснейшых прыкладаў. Ён не прымушвае вас выбіраць адзін фіксаваны дизайн табелі. У замен ён забезпечвае логіку і кераванне станам для такіх задач, як:
- Сортаванне
- Фільтрацыя
- Стронічаванне
- Выбіранне рэядкаў
Ператварэнне гэтаго стану ў фактычны маркап залежыць або толькі ад вас. Напрыклад:
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{/* Your own rendering logic */}
</tbody>
</table>
Точная структура API тут не так важна. Гэта, што мае значэння, — глыбокая архітектура: дварчык табелі керуе ўсімі дзействамі, а вы — яго візуальным прадстаўленнем. Паколькі гэтыя два аспекты ўскладнены разам, вы можете свабодна перадзяліць візуальную частыню, не патрабуючы, каб основная логіка табелі ператворылася ў вялічезны візуальны элемент.
6. Раз’ядраць дзействанне ад візуальнага прадстаўлення
Гэта сама сабою ведае да ўсёй іншай рэкамендаціі, якую варта выконваць:
Раздзеляйце тое, што робіць компонент, ад таго, як ён візуальна прадстаўляецца.
Одні зусім компоненты можа маты значную внутраню складнасць, якая включае такія аспекты, як:
- Навігацыя за дапамогою клавіатуры
- Управленьне фокусам
Якісна обработка всіх гэтых аспектаў рэдка калі ёст моцна простая. Але ні адны з гэтых элементаў не вымагае, каб той самы компонент таксама вялічка наказваў, як самэў канечны рэзультат павінен выглядаць.
Гэтае ўсё — галоўная ідея большасці безгалавых систем UI. Їхню працэздатнасць можна перадаць у разных візуальных реалізацыях, пры чым шар представлення застаецца абсалютна відкрытым. Такое роздзеленне зазвычай дае значна лепшую можлівасць перадачы, чым прабаўка стварыць адны вялічкі компонент, заполнены дзесяткамі налашоўкаў візуальнага канфігурацыі.
7. Калі props дзейсна маюць сэнс
Усё гэта не значыць, што props — гэта паслаблены падход.
Это адны з найфундаментальнейшых інструментаў, якія дае вам React.
Проблема не ў тым, што існуюць props. Проблема ў тым, калі іх викорыстоўваюць для закодавання кожнай можлівай структурной варыяцыі, якая можа знадобіцца компоненту.
Такі прыклады ўсё цілком нормальныя:
<Button variant="primary" />
<Button size="sm" />
<Input disabled />
<Card className="featured" />
Чырвоны пазнак з’яўляецца, калі компанента начынае накапліваць такі список:
<Component
showHeader
showFooter
showBadge
showIcon
showActions
iconPosition="left"
badgePosition="right"
footerAlignment="center"
actionLayout="horizontal"
compact
dense
bordered
rounded
disableAnimation
customHeaderRenderer={...}
customFooterRenderer={...}
/>
Калі вы пазнаёте такі шаблон, варта на момент зупыніцца і запытаць:
"Чы рэальна ўсё гэта застацца як пропсы, чы можа дзеякі з іх краща будуць выкарыстоўваны як складовыя дзеціны?"
Проста запытанне гэта часта дапамагае ухіліцца ад багато непатрэбнай складнасі паходзячы.
8. Чаго насправды коштае занадто большая абстракцыя
Рэальныя нашкоды, якія вызвалі занадта скаладныя компаненты, не ў візуальнай частцы.
Это тое, што іх стае даскладна мэнтаваць.
Шырока распраўляючаяся компанента мае вялікі радыус уплыву. Якщо двадцать месцаў у вашай базе коду залежаць ад яе, тады кожны новы пропс, які вы дадаеце, можа паўзіць на ўсіх тых двадцох корыстувачах.
Інжынеры становяцца нерашучымі, калі трэба яе змяніць.
Адаптаванне дакументацыі для яго стае сложней.
Тэставанне яго стае сложней.
Адказванне на багі ў яму стае сложней.
У канцы канцоў такі компонент стае настолькі „перадуменным“, што ніхто насправды не хоча яго перадуменваць.
Гэта іронія, якая ўжо є частью гэтага патэрану.
Чрэзмерная абстракцыя можа паслабіць саму перадуменнасць, якую ён меў бы забезпечыць.
Маленькі, чыста спрямованы прымітіўскі элемент часта виявляецца болей перадуменным на практыцы, чым вялікі компонент, створаны для адрабаткі кожнай можлівай варіяцыі.
Практычная рамка для стварэння компонентаў
Перш чым вжываць яшчэ адну настройкавую параметр, пераканайцеся, што адбыліся гэтыя пераказы.
1. Я маю справу з поведэнням чы з візуальным аспектам?
Якщо мова працюе працэвыкладання, тад проп або спецыяльны хук, верагацельна, ўсё-такі будуць правымі інструментамі. Якщо ж мова працэўка чыста пра візуальную структуру, тад композіцыя зазвычай ўсё-такі будзе кращым выкарыстоўваннем.
2. Чы рэальна гэты компонент патрабуе гэтых даноў?
Якща ад однаго компонента чакаюць, што ён будзе розумець падказкі, хуки аналітыкі, значкі, іконкі, показчыкі завантажэння і кожную распарадку своіх дзецячых элементаў, тад ён практычна напэўна взяў на сябе больш адпаведальнасці, чым трэба.
3. Чы children могуць заместа гэтага выконваць гэту функцыю?
Якщо так, тад краща выкарыстоўваць композіцыю, чым прынямаць ўсё новы проп, які кантролюець формат контэнту.
4. Чы іменаваныя слоты могуць павышыць яснасць API?
Для болей складных компонентаў вялікі прагтэматычны ўзначэння частак, такіх як:
<Card.Header />
<Card.Body />
<Card.Footer />
можа значна спрыяць лепшаму розуменню структуры.
5. Чы логіку можна адсунуць ад маркапаў?
Якщо так, то варта ўвагі падходзь з безгалавым дизайном або акцэнтам на логіку.
6. Чы рэшае гэты элемент рэальную, чы выдуманую проблему?
Гэты пытанне мае большое значэнне, ніж здаецца.
Утримайцеся ад дадзення чаго-небудзь такога:
futureFeature={true}
толькі таму, што хтось можа захацець гэта ў будучыні.
Проектаваць трэба на адной основе рэальных вакуумоў, а не тых, якія вы толькі спадзяваецеся.
Заключныя меркі
Мета стварэння повтарна вжываемых компонентаў React — не створыць ўсемогутні элемент, а вырашыць стварыць часткі, якія можна склadaць разнымі спосабамі.
Гэта разлік здаецца тонкім, але з архітэктурнай точка зору ён меняе все.
Компанент, які выкарыстоўвае трыццаць параметраў, можа здавацца гнучкім на першы погляд, але кожны з эых параметраў прыносіць новую адпаведнасць, новы стан, які трэба врачываць, і ўсё больша колькість комбінацыяў, якія рано чым даведзецца падтрымаць.
Натоўпіс, компанент з вузкім API і чыстымі межамі складання можа здавацца простым.
Але на практыцы ён зазвычай вытрымаеся набагато лепша з часам.
Таму, калі наступны раз у вас з’явится бажанне дадаць ўшырэйшы параметр да спільнага компанента, зупініцеся на момент і запытайце ся:
„Чы не роблю я гэты компанент толькі болей настраиваемым, чы не роблю яго болей відновімым?“
Этыя два пункты не ўзаемназаменны.
Компанент не стае відновімым, прыкупляючы трыццаць опцый.
Ён стае відновімым, добра выкананыя адна задача, чыткія межы і вядома адстаўка ад непатрэбных функцыяй.
Архітектура солідных компонентов не спрямована на прагнозаванне кожной майбутняй патрэбы.
Яна спрямована на тое, каб усі майбутні патрэбы, якія з’являюцца, было лёгка склadaць.