iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0
Modern Web

剛入行就一人重新打造公司前端系統?系列 第 9

Day 9 - 透過 Container/Presentational Pattern 來實現功能與樣式分離

  • 分享至 

  • xImage
  •  

接續昨天討論的共用元件,今天會實作更複雜一些的元件,並且使用 Container/Presentational Pattern 實現功能與樣式的分離。

製作可以「共用」的元件

在 Day 8 中,我們提到一個問題:「當元件功能類似時,應該拆成多個元件,還是設計一個元件來支援不同變體 (variants)?」舉個進階的 Table 為例,假設你需要支援以下三種功能:

  1. 單純的資料顯示
  2. 單選 (radio) column
  3. 多選 (checkbox) column

應該把這些功能拆成三個不同元件,還是讓一個元件支援這三種功能呢?為了提高維護性和減少重複程式碼,最好的做法是設計一個通用元件。

實作 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",或空字串展示純資料 */ 
/>

codesandbox

上述的寫法為了支持單純的資料顯示、支持 radio column、支持 checkbox column 三種情境,使用 selectionType 來控制了,透過條件渲染來控制不同的情境,但似乎違反了昨天提到的「避免過度依賴條件渲染」。

另外,Day 8 也提到過一個問題是 「props 應該要傳哪些東西才合適?」,上述的寫法會需要在 <Table> 中就要放onSelectselectionType 這類 props,這樣的設計可能會導致元件的職責過於廣泛,在這種情況下可以怎麼做呢?

Container/Presentational Pattern

可以使用 Container/Presentational Pattern 這個 React 的設計模式,來將視圖(view)與應用程式邏輯(application logic)分開,來達到功能與樣式分離。

Container/Presentational Pattern 將元件分為兩類:

  1. Presentational Components(展示元件):專注於渲染 UI,只負責接收 props 並展示內容,不處理邏輯或狀態管理。<Table> 屬於這一類,負責渲染表格結構、樣式等,而不直接處理選擇、排序、過濾等邏輯。
  2. Container Components(容器元件):負責處理邏輯和狀態管理,將處理後的資料和回調函式傳給展示元件。例如,容器元件會處理資料的選擇、篩選或排序,然後將處理好的資料和行為傳給 <Table> 等展示元件。

1. 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>
  );
};

2. Container(容器元件)

容器元件負責處理選擇邏輯,並將處理後的回調傳給 <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)}
        />
      )}
    />
  );
};

codesandbox

參考資料


上一篇
Day 8 - 製作共用元件很難嗎?從避免過度依賴條件渲染開始!
下一篇
Day 10 - Compound Pattern
系列文
剛入行就一人重新打造公司前端系統?31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言