# 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 對樣式都有影響的話,需要好好規劃每一個參數實際影響渲染的哪一個部分,終極目標是寫出直覺、方便、好用的元件。