iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
自我挑戰組

React 入門到實作與除錯|30 天哩ㄟ刻系列 第 17 篇

Day 17 : 有福同享,讓元件共享 state

  • 分享至 

  • xImage
  •  

有難的時候,先不用共享沒關係,我已經夠多了 /images/emoticon/emoticon07.gif

今天目標

  1. 找到共享 state 的共同外層元件
  2. 拆出訂單清單與檢視元件
  3. 用事件回呼通知外層元件更新
  4. 同時控制介紹的開合狀態

找到共享 state 的共同的外層元件

首先來看「拆分元件」與「共享 state」各自意圖:

  • 拆分元件:為了讓畫面職責、閱讀或重用更清楚。
  • 共享 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 要協調成最多一個展開

React:Sharing State Between Components

拆出訂單清單與檢視元件

知道要拆分哪些元件後,接著來新增:

// 示意片段
<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 對應到 OrderList props 收到的 handleChangeQuantity

當按下之後,會依序發生:

  1. OrderList 呼叫收到的 onChangeQuantity(item.id, 1)。
  2. 這個 prop 指向外層元件的 handleChangeQuantity。
  3. 外層元件用 map() 建立新訂單清單,再呼叫 setOrders。
  4. 外層元件重新渲染,重算總計並用同一個 ID 查出最新項目。
  5. 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 決定內層元件目前的值。

React:Controlled and uncontrolled components


今天的算是概念,主要是學會怎麼在合適的時候拆元件,以及元件之間需要共用 state 時可以怎麼做,清單與檢視的操作結果維持昨天的行為,但其實元件已經拆開。

明天接著看元件的位置、類型與 key,觀察切換對象時,state 何時保留、何時重設。

參考資料


上一篇
Day 16 : 去蕪存菁,整理 state 的結構
系列文
React 入門到實作與除錯|30 天哩ㄟ刻 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言