有難的時候,先不用共享沒關係,我已經夠多了
首先來看「拆分元件」與「共享 state」各自意圖:
各自意圖不同,以今天要處理的 DrinkOrder 來看。
原本是:
DrinkOrder
├─ DrinkCounter(紅茶、綠茶各一個)
│ └─ DrinkName(每個計數器各一個)
└─ DrinkDetails(紅茶、綠茶各一個)
主元件DrinkOrder是由多個功能組成的:
整個點餐畫面,保存訂單、選取 ID、取餐資料與送出結果、直接寫出清單、檢視區與表單。
這樣功能太多,一次要維護很大的元件,現在我們把這個大元件再拆分。觀察到原本的「訂單清單」、「目前檢視的項目」與「取餐資料」都是 DrinkOrder 內的 <section>。把前兩個區塊拆成 OrderList 與 OrderPreview。
拆完之後的元件關係:
DrinkOrder
├─ DrinkCounter(紅茶、綠茶各一個)
│ └─ DrinkName(每個計數器各一個)
├─ DrinkDetails(紅茶、綠茶各一個)
├─ OrderList(訂單清單)
└─ OrderPreview(目前檢視的項目)
先判斷哪些元件需要使用這份 state,再決定放在哪裡,並且每一份 state 都要清楚知道,由哪個元件保存並負責更新。
如果只有一個元件需要保存與操作 state,就留在該元件,例如各自的待加入杯數。如果多個內層元件需要使用同一份資料,或操作必須互相協調,就由它們共同的外層元件保存,再透過 props 傳給它們。
例如外層元件 DrinkOrder 保存 orders,內層元件 OrderList 接收完整清單,OrderPreview 接收從清單查出的選取項目。需要修改訂單時,內層元件呼叫外層元件提供的回呼,由外層元件更新 orders,兩個區域再收到最新內容。
表格整理如何判斷是否拆分元件與共用state:
| 資料 | 位置 | 判斷 |
|---|---|---|
orders、selectedOrderId |
DrinkOrder |
清單與檢視都需要,而且刪除、清空與送出也需要使用 |
| 待加入杯數與規格 | 各自的 DrinkCounter |
紅茶與綠茶可以獨立調整,加入清單才通知外層元件 |
| 目前展開哪個介紹 | 提升到 DrinkOrder |
兩個 DrinkDetails 要協調成最多一個展開 |
知道要拆分哪些元件後,接著來新增:
// 示意片段
<OrderList
orders={orders}
selectedOrderId={selectedOrderId}
totalQuantity={totalQuantity}
totalPrice={totalPrice}
onSelect={handleSelectOrder}
onChangeQuantity={handleChangeQuantity}
onSugarFree={handleOrderSugarFree}
onDelete={handleDeleteOrder}
onClear={handleClearOrders}
onReportLater={handleReportOrdersLater}
/>
<OrderPreview
order={selectedOrder}
onClearSelection={handleClearSelection}
/>
OrderList 不保存自己的清單,OrderPreview 不保存另一份選取項目。直接使用當次收到的 props,且 order 允許 find() 找不到時回傳 undefined,並在畫面中判斷是否有資料。
例如 OrderPreview 直接讀 order,不用再寫一個 useState(order),外層元件的 orders 更新後,用 ID 查到新項目,再傳給它就好。
在 DrinkOrder 裡新增兩個處理函式給新元件使用:
function handleSelectOrder(id: string) {
setSelectedOrderId(id);
}
function handleClearSelection() {
setSelectedOrderId(null);
}
我們新增的元件已經指向這兩個函式,分別是在 onSelect 和 onClearSelection。
內層元件可以透過 props 呼叫外層的 function,例如當清單中的「此筆加一杯」會呼叫收到的函式:
<button type="button" onClick={() => onChangeQuantity(item.id, 1)}>
此筆加一杯
</button>
onChangeQuantity對應到OrderListprops 收到的handleChangeQuantity
當按下之後,會依序發生:
OrderList 呼叫收到的 onChangeQuantity(item.id, 1)。handleChangeQuantity。map() 建立新訂單清單,再呼叫 setOrders。OrderList 收到新清單,OrderPreview 收到新項目,兩區都顯示新杯數。外層元件透過 props 把資料與回呼傳給內層元件,內層元件呼叫收到的回呼,通知外層元件處理事件,外層元件更新 state 後,再傳入最新資料。
原本每個 DrinkDetails 有自己的 isExpanded,所以兩個介紹可以同時開啟,現在我們試著以共享 state 來改看看。
以「最多一個展開」,把決定權移到 DrinkOrder 保存目前展開的飲料 ID,null 表示全部收合:
// 示意片段
// 型別放在元件外
type DrinkId = "black-tea" | "green-tea";
// Hook 與事件函式放在 DrinkOrder 裡。
const [expandedDrinkId, setExpandedDrinkId] = useState<DrinkId | null>(null);
function handleToggleDetails(id: DrinkId) {
setExpandedDrinkId((prevId) => (prevId === id ? null : id));
}
刪掉內層元件自己的開合 Hook 與原本的切換函式,改成接收 isExpanded 和 onToggle。
例如紅茶的接法是:
<DrinkDetails
name="紅茶"
description="茶香濃郁,口感醇厚。"
isExpanded={expandedDrinkId === "black-tea"}
onToggle={() => handleToggleDetails("black-tea")}
/>
綠茶則用 green-tea。外層元件的同一個 ID 一次只會符合其中一個介紹,再按已展開的介紹,ID 回到 null,兩個都收合。
現在開合由 props 控制,可以稱為受控元件。與表單輸入的 value 相同,都是 由外層元件透過 props 決定內層元件目前的值。
今天的算是概念,主要是學會怎麼在合適的時候拆元件,以及元件之間需要共用 state 時可以怎麼做,清單與檢視的操作結果維持昨天的行為,但其實元件已經拆開。
明天接著看元件的位置、類型與 key,觀察切換對象時,state 何時保留、何時重設。