剛開始寫 React 時,我對 Component 的理解很單純:
畫面太大,就拆。
於是看到一個很長的檔案,很自然會覺得:
500 行
↓
太長
↓
拆 Component
但真的進到專案後才發現,事情沒有這麼單純。
有些 Component 雖然不長,卻同時處理很多不同責任;
有些 Component 看起來很長,但裡面的內容其實高度相關,硬拆反而讓資料到處傳。
所以我後來開始把問題從:
「這個檔案是不是太長?」
改成:
「這一塊是不是已經有自己的責任?」
假設現在有一個 Detail Page:
function DetailPage() {
return (
<>
<BasicInfo />
<ProductInfo />
<DocumentInfo />
<PerformanceInfo />
</>
);
}
一開始它可能不是這麼漂亮。
比較可能是所有東西都直接寫在同一個檔案:
function DetailPage() {
return (
<>
<Card title="Basic Information">
{/* 很多欄位 */}
</Card>
<Card title="Product Information">
{/* 很多欄位 */}
</Card>
<Table
columns={...}
dataSource={...}
/>
<Card title="Document">
{/* 又很多欄位 */}
</Card>
</>
);
}
功能繼續增加之後,還可能出現:
DetailPage
├─ API Request
├─ Form
├─ Basic Information
├─ Product Information
├─ Table
├─ Document
├─ Permission
├─ Button
├─ Modal
└─ 各種處理 function
這時候真正麻煩的,不只是「檔案很長」。
而是我想修改其中一塊時,要在大量不相關的程式碼裡尋找。
例如只是想修改:
Product Information
卻要先穿過:
API
↓
Permission
↓
Basic Info
↓
各種 function
↓
終於找到 Product
這時候就開始出現拆分的價值。
例如:
DetailPage
│
├─ BasicInfo
├─ ProductInfo
├─ DocumentInfo
└─ PerformanceInfo
這些區塊都有一個特徵:
我可以清楚說出「它負責什麼」。
例如:
BasicInfo
→ 負責顯示基本資料
ProductInfo
→ 負責商品相關內容
DocumentInfo
→ 負責文件資訊
當一塊 UI 已經可以被清楚命名,而且擁有自己的資料與畫面邏輯時,就很適合考慮拆成 Component。
例如:
function DetailPage() {
return (
<>
<BasicInfo data={detail} />
<ProductInfo products={detail.products} />
<DocumentInfo documents={detail.documents} />
</>
);
}
這時候 DetailPage 的責任也開始變得比較清楚:
DetailPage
↓
取得 / 管理主要資料
↓
把需要的資料交給不同 Component
而子 Component:
ProductInfo
↓
只需要關心 Product
這其實就接回上一篇的 State 和 Props。
這是我在實際專案裡很有感的一件事。
以前所有東西都放在同一個檔案時:
API Response
↓
一大堆變數
↓
一大堆 JSX
我其實很難看出:
哪一份資料到底是誰在用?
但拆 Component 時,就會被迫回答:
BasicInfo 需要什麼?
ProductInfo 需要什麼?
DocumentInfo 需要什麼?
例如:
<ProductInfo
products={detail.products}
currency={detail.currency}
/>
光看 Props 就開始能知道:
ProductInfo
│
├─ products
└─ currency
它依賴的資料比較明確。
所以拆 Component 不只是整理檔案。
對我來說,它還有另一個很重要的作用:
幫助我重新整理每個區塊真正需要哪些資料。
學到 Component 之後,也很容易走到另外一個極端。
例如:
<div>
<Title />
<Name />
<Price />
<Button />
</div>
然後每一個都建立一個檔案:
Title.tsx
Name.tsx
Price.tsx
Button.tsx
最後想找一個畫面,反而要一直跳檔案。
所以:
Component 不是拆越多越好。
我現在會先問幾個問題:
① 這一塊有明確的責任嗎?
② 它是不是有自己的一組資料或邏輯?
③ 未來修改它時,能不能大部分只關心這個 Component?
④ 這一塊是不是會被重複使用?
其中第 ④ 點只是其中一個理由。
以前我會以為:
「只有重複使用才需要拆 Component。」
但後來才發現,即使一個 Component 只使用一次,只要它有獨立責任,拆開依然可能讓程式更容易閱讀與維護。
我曾經遇過一個頁面,功能一路增加之後,越來越多內容都集中在同一個地方。
後來開始按照畫面與功能責任,把不同區塊拆出去:
Page
│
├─ Info Section
├─ Product Section
├─ Document Section
└─ Other Section
但拆完並不是結束。
真正麻煩的問題反而接著出現:
這個 Component 需要哪些資料?
這份資料要從 Parent 傳嗎?
誰負責修改?
有沒有資料其實很多 Component 都需要?
也就是上一篇提到的:
State
↓
Props
↓
Child Component
開始真的出現在實際開發裡。
這時候我才慢慢理解:
拆 Component 不是把 JSX 剪下來貼到另一個檔案而已。
真正的拆分還包含:
責任重新分配
+
資料依賴重新整理
遇到比較大的頁面時,比起直接開始搬程式碼,我會先簡單整理:
DetailPage
│
├─ BasicInfo
│ └─ basicData
│
├─ ProductInfo
│ ├─ products
│ └─ currency
│
└─ DocumentInfo
└─ documents
然後再問:
哪些資料只有自己需要?
↓
留在 Component
哪些資料 Parent 已經有?
↓
透過 Props 傳入
哪些資料很多深層 Component 都需要?
↓
可能要再想其他管理方式
最後這個「很多 Component 都需要」的問題,就會帶出下一個我以前看到很害怕的東西:
useContext(...)
以前我看到 Context,只覺得:
又多一個 React 咒語。
但真的遇到 Component 一層一層拆下去之後,才開始理解它到底想解決什麼。
下一篇就來看:
為什麼 Props 傳一傳,最後會傳到懷疑人生?Context 到底是在解決什麼問題?