接續昨天討論的共用元件,今天會實作更複雜一些的元件,並且使用 Container/Presentational Pattern 實現功能與樣式的分離。
在 Day 8 中,我們提到一個問題:「當元件功能類似時,應該拆成多個元件,還是設計一個元件來支援不同變體 (variants)?」舉個進階的 Table 為例,假設你需要支援以下三種功能:
應該把這些功能拆成三個不同元件,還是讓一個元件支援這三種功能呢?為了提高維護性和減少重複程式碼,最好的做法是設計一個通用元件。
以下是一個支援單選和多選的 Table
元件範例:
const Table = ({ columns, data, selectedItems, onSelect, selectionType }) => {
return (
<table>
<thead>
<tr>
{selectionType && <th></th>} {/* 如果有選擇類型才顯示選框列 */}
{columns.map((column) => (
<th
key={column.key}
>
{column.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.id}>
{/* 單選功能 */}
{selectionType === "radio" ? (
<td>
<input
type="radio"
name="selection"
checked={selectedItems.includes(item.id)}
onChange={() => onSelect(item.id, selectionType)}
/>
</td>
) : /* 多選功能 */
selectionType === "checkbox" ? (
<td>
<input
type="checkbox"
checked={selectedItems.includes(item.id)}
onChange={() => onSelect(item.id, selectionType)}
/>
</td>
) : /* 純粹展示資料 */ null}
{columns.map((column) => (
<td key={column.key} className="px-6 py-4 whitespace-nowrap">
{item[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
// 在頁面中使用
<Table
columns={columns}
data={data}
selectedItems={selectedItems}
onSelect={handleSelect}
selectionType="" /* 該屬性可以為 "radio"、"checkbox",或空字串展示純資料 */
/>
上述的寫法為了支持單純的資料顯示、支持 radio column、支持 checkbox column 三種情境,使用 selectionType
來控制了,透過條件渲染來控制不同的情境,但似乎違反了昨天提到的「避免過度依賴條件渲染」。
另外,Day 8 也提到過一個問題是 「props 應該要傳哪些東西才合適?」,上述的寫法會需要在 <Table>
中就要放onSelect
和 selectionType
這類 props,這樣的設計可能會導致元件的職責過於廣泛,在這種情況下可以怎麼做呢?
可以使用 Container/Presentational Pattern 這個 React 的設計模式,來將視圖(view)與應用程式邏輯(application logic)分開,來達到功能與樣式分離。
Container/Presentational Pattern 將元件分為兩類:
<Table>
屬於這一類,負責渲染表格結構、樣式等,而不直接處理選擇、排序、過濾等邏輯。<Table>
等展示元件。<Table>
不直接處理選擇邏輯,只負責顯示資料和選項欄:
const Table = ({ columns, data, renderSelection = null }) => {
return (
<table>
<thead>
<tr>
{renderSelection && <th>選擇</th>}
{columns.map((column) => (
<th key={column.key}>{column.title}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.id}>
{renderSelection && <td>{renderSelection(item)}</td>}
{columns.map((column) => (
<td key={column.key}>{item[column.key]}</td>
))}
</tr>
))}
</tbody>
</table>
);
};
容器元件負責處理選擇邏輯,並將處理後的回調傳給 <Table>
:
const TableContainer = ({ columns, data, selectionType }) => {
const [selectedItems, setSelectedItems] = useState([]);
const handleSelect = (id) => {
if (selectionType === 'radio') {
setSelectedItems([id]);
} else if (selectionType === 'checkbox') {
setSelectedItems((prev) =>
prev.includes(id)
? prev.filter((itemId) => itemId !== id)
: [...prev, id]
);
}
};
return (
<Table
columns={columns}
data={data}
renderSelection={(item) => (
<input
type={selectionType}
checked={selectedItems.includes(item.id)}
onChange={() => handleSelect(item.id)}
/>
)}
/>
);
};