# React 元件外部擴充樣式寫法
Elantris
[TOC]
---
## 前言
我經手的專案大都是需要高度客製化樣式的網站,我的策略都是先看過所有網站設計圖,整理出不斷重複出現的元件,構築這個網站專用的元件庫。後續在串接後端資料、實作操作流程時都能直接引入元件維持網站整體一致的外觀設計。
在研究了幾個 UI Library 之後我試圖找出各家寫法上的差異,簡化成自己比較容易理解、擴充、維護的寫法,日後我在自建網站專用元件庫時可以參考。
## 方案
UI Library 的原則是盡可能將每個元件寫得乾淨一點,不跟任何系統功能有耦合,所有資料都是透過 props 帶入。以下提供兩個想法,實務上我都是同時混搭兩種寫法。
### Variant/Theme
根據設計圖事先準備好固定的變體與主題,在取用元件時透過改變 `variant` 或 `theme` props 決定要用哪一個組合。
這個方法的優缺點也很明顯,主要目的在於限縮組合數量,設計團隊可以比較容易掌握實際的排版結果、工程團隊不用處理過於自由奔放的樣式。但相對地代表限縮了延展性,每當遇到需求有不同的樣式都要改動元件本身的結構或判斷。
```tsx
const Panel: React.FC<{
variant?: 'default' | 'glory'
theme?: 'default' | 'light' | 'dark'
title?: React.ReactNode
children?: React.ReactNode
footer?: React.ReactNode
}> = ({ variant = 'default', theme = 'default', title, children, footer }) => {
/* ... */
}
```
渲染的部分就是 conditional rendering 自由發揮,例如:
1. 不同的結構
```tsx
if (variant === 'glory') {
return (
<div className="panel panel-glory">
{/* ... */}
</div>
)
}
return (
<div className="panel">
<div className="panel-title">
{/* ... */}
</div>
</div>
)
```
2. 對應的 className/style 組合
```tsx
return (
<div
className={`panel ${variant === 'glory' ? 'panel-glory' : ''}`}
style={theme === 'light' ? { background: 'white' } : { background: 'black' }}
/>
)
```
```tsx
const classNames = {
variant: {
default: '',
glory: 'panel-glory',
},
theme: {
default: '',
light: 'panel-light',
dark: 'panel-dark',
},
}
return (
<div className={`panel ${classNames.variant[variant]} ${classNames.theme[theme]}`} />
)
```
### ClassName/Style
直接把 `className` 與 `style` 當作 props 傳遞,本身有預設的樣式,在取用元件時可以任意存取元件結構中的 `className` 與 `style` 屬性,客製出畫面需求當下需要的元件外觀。
這個寫法雖然對樣式的自由度高,相對地會犧牲元件的重複使用性,而且通常不太方便改變元件的渲染結構。
常見的寫法會把結構中每一個 Element 的 className 變成對應 props:
```tsx
<Dialog
wrapperClassName=""
headerClassName=""
closeIconClassName=""
titleClassName=""
bodyClassName=""
footerClassName=""
/>
```
或是整合起來:
```tsx
<Dialog
classNames={{
wrapper: '',
header: '',
closeIcon: '',
title: '',
body: '',
footer: '',
}}
/>
```
渲染部分通常會選擇擴充 (extend),也可以用取代 (replace) 的形式,端看元件渲染要如何接納這個外部帶進來的參數:
```tsx
return (
<div className={`dialog-header ${classNames?.header ?? ''}`}>
{/* ... */}
</div>
)
```
### Utility-first
其實跟上述兩種的寫法差不多,就是 Tailwind 特化板的寫法,可以搭配 [tailwind-merge](https://github.com/dcastil/tailwind-merge) 來組出實際渲染的 className。
```tsx
import { twMerge } from 'tailwind-merge'
// ...
return (
<div
className={twMerge(
'flex items-center p-4',
variant === 'glory' && 'ring-2 ring-blue-500/50',
wrapperClassName,
)}
/>
)
```
## 結語
時時刻刻提醒自己幫特定網站寫專用 UI Library 就是在管理全站的顯示元件,目標是提供單純的元件給共同開發者方便取用,在實作頁面功能時能維持一致的操作邏輯或樣式外觀。
使用 variant/theme 可以提供預先設計好的樣式組合;使用 className/style 提供完全客製化的介面。同時混用兩種寫法也是完全沒有問題的,只是當不同來源的 props 對樣式都有影響的話,需要好好規劃每一個參數實際影響渲染的哪一個部分,終極目標是寫出直覺、方便、好用的元件。