# TBC 線下技術小聚:React Render Props Design Pattern
> Date: 2025/07/19
> 主講人:Zet|主任前端工程師 @ iCHEF
> 主題簡介:深入解析 React Render Props 設計模式,探討其與 HOC、Hooks 的比較,並提供實戰範例與設計哲學。
> 👉 [教學簡報](https://zet-chou.notion.site/TBC-React-render-props-design-pattern-1d1a3f5665b080ddbcacf9e320854211)
## Design Pattern
一些被歸納出來好用的程式設計方法,在經過反覆驗證之後,仍具有使用價值,並且可以解決某些特定問題。
### Facade Pattern
可以參考自己的筆記:[設計模式:Facade Pattern(外觀模式)](https://hackmd.io/bIrh-R1gTe2MmuZOukqwlg?view#%E8%A8%AD%E8%A8%88%E6%A8%A1%E5%BC%8F%EF%BC%9AFacade-Pattern%EF%BC%88%E5%A4%96%E8%A7%80%E6%A8%A1%E5%BC%8F%EF%BC%89)
Facade Pattern(外觀模式)是一種**結構型設計模式**,目的是提供一個統一的介面,讓使用者可以更簡單地操作一組複雜的子系統,**隱藏內部實作細節,簡化對外操作流程**。
**適用情境與優點**
* 對於邏輯複雜、有多個子系統需要協調的場景非常有用
* 隱藏複雜邏輯與細節,讓使用者僅透過 Facade 就能完成流程操作
* 幫助建立清晰的系統邊界與模組責任
* 提高子系統的封裝性與維護性
> 是用來「藏技術細節」,但不代表要「把所有邏輯都塞進去」。
**不適用情境**
1. **邏輯極簡或單層結構的專案**:可能會造成架構過度複雜。
2. **內部邏輯頻繁變動或流程排列組合多樣時**:Facade 可能難以維持穩定性與抽象性。
3. **無固定流程的場景**:容易導致 Facade 無法有效抽象出流程。
**常見誤用情況**
1. **Facade 裡面放入過多雜項邏輯**,失去簡化與抽象的本意
2. **利用 Facade 掩蓋內部系統設計混亂**,反而隱藏技術債
3. **建立過多層級的 Facade**,造成維護與理解負擔
**Best Practice**
* 職責明確、大小適中
* 只負責統整流程的調用,不承擔實際邏輯計算
* 關注子系統與外部 API 的存取權限與擴充性
**單元測試觀點**
在實作 Facade Pattern 時,可透過測試角度檢視是否設計合理:
> 如果你需要寫大量的 mock 才能讓一個 Facade 測得動,這可能表示其**相依性過高、抽象層級錯誤或責任不夠單一。**
## Inversion of Control(控制反轉)
**IoC(Inversion of Control)控制反轉**,是一種設計思想,意思是將原本由主程式控制的流程交由**外部機制或函式反過來主導**,使主程式不再掌握每一個細節,而是**提供設定、規則或回呼函式(callback)**,讓外部控制具體的執行流程。
### 傳統流程(正常控制流程)
在一般程式控制中,主程式掌控所有邏輯流程,包含呼叫外部函式、接收回傳值,並繼續後續操作:
```plaintext
main → 呼叫外部函式 → 等待回傳 → 繼續主程式邏輯
```
### 控制反轉(IoC)
在 IoC 中,主程式只提供設定或處理邏輯的「描述」,流程則由外部框架、函式主導:
```plaintext
main → 呼叫外部函式 → 外部函式主導後續執行流程(回呼主程式提供的邏輯)
```
### 範例比較:for vs map
傳統 `for` 迴圈:主程式自行控制流程(沒有 IoC)
```js
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
arr2.push(arr1[i] * 2);
}
console.log(arr2); // [2, 4, 6, 8, 10]
```
➡️ 主程式掌控整個流程:索引、邏輯與資料操作。
---
`map`:流程由外部函式(map)主導(IoC)
```js
const arr1 = [1, 2, 3, 4, 5];
const arr2 = arr1.map((item) => item * 2);
console.log(arr2); // [2, 4, 6, 8, 10]
```
➡️ 主程式只提供邏輯(`item * 2`),由 `map` 控制整體執行流程與迭代。
> 有點像:**你交出資料與邏輯描述,外部函式或框架幫你跑流程。**
* 回呼函式(callback)、事件驅動(event handler)、Promise / async 都是 IoC 的例子
* 宣告式語法也帶有 IoC 的特質:
* **SQL**:描述你想要什麼資料,由資料庫決定如何查
* **CSS**:描述畫面樣式,由瀏覽器解析並渲染
* **React JSX**:描述 DOM 結構與行為,由 React 決定更新邏輯
## Dependency Injection(依賴注入)
**Dependency Injection(DI)** 是一種設計模式,用來將物件所依賴的元件(如資料、邏輯、設定)**從外部注入**,而不是在元件內部自己創造或控制。
這種模式的核心目的是:**降低耦合度、提高重用性與測試性。**
### 和 Inversion of Control(IoC)的關係?
DI 可以視為 IoC 的一種實踐方式。當一個元件把「控制邏輯」交由外部傳入,就實現了控制反轉。
## 實作範例:React 中的依賴注入
```jsx
// Button 元件:依賴「參數」來顯示內容與執行邏輯
function Button({ text, onClick }) {
return <button onClick={onClick}>{text}</button>;
}
```
### 說明:
* `Button` 本身並不知道點下去要做什麼
* `Button` 不自行產生文字或邏輯,而是**接收外部傳入的依賴(props)**
* 這就是所謂的「依賴注入」
### 使用場景
```jsx
function App() {
const handleClick = () => alert('hello');
return <Button text="I am a button" onClick={handleClick} />;
}
```
### 分析:
* `App` 將兩個「依賴」注入到 `Button`:
* 顯示文字 `text`
* 點擊時執行的函式 `onClick`
* `Button` 僅僅「使用」這些依賴,不需要知道它們是從哪裡來的,也不需要自己創造
### DI 的好處
- **元件更抽象、更專注本職責**
- **方便測試與重用**(因為依賴可替換)
- **減少耦合**,元件之間互不依賴具體實作,只依賴介面(或形狀)
:::success
* **React 中的 props 機制就是一種天生的 DI 模式**
* 除了 props,也可用 context、hook、HOC 等手段實現依賴注入
* 如果一個元件裡面寫死邏輯(如直接呼叫 `alert()`),那麼它就難以測試與重用,也違反了 DI 的精神
:::
## React Render Props
`Render Props` 是一種 **共用邏輯(Stateful Logic Reuse)** 的設計模式。
簡單來說:**將 UI 的渲染權交給父層元件注入**,而不是子元件自己決定怎麼畫。
### 觀念連結:
* 屬於一種實踐 **Dependency Injection(DI)** 與 **Inversion of Control(IoC)** 的模式
* 與 `HOC(高階元件)` 同樣是邏輯重用的手段,但更具彈性
### 基本使用方式
```jsx
function Child({ children }) {
// 子元件把控制權交給父元件的 render function(children)
return children();
}
function Parent() {
return (
<Child>
{() => <div>Hello from render props</div>}
</Child>
);
}
```
### 優點
1. 避免命名衝突
使用 Render Props 可以讓你**自定義參數名稱**,不像 HOC 那樣容易造成 props 汙染或命名衝突。
```jsx
function WithUser({ children }) {
const user = { name: '祐霖', age: 25 };
return children(user);
}
function App() {
return (
<WithUser>
{(customUser) => <div>Hello, {customUser.name}</div>}
</WithUser>
);
}
```
2. 資料來源透明、明確
能清楚看出資料是從哪個邏輯組件而來,減少魔法與黑盒。
### 缺點
1. JSX callback hell(結構巢狀複雜)
如果有多層 render props,會造成巢狀過深、難以閱讀。
```jsx
<WithUser>
{(user) => (
<WithTheme>
{(theme) => (
<WithLocale>
{(locale) => (
<div style={{ color: theme.text }}>{user.name} - {locale}</div>
)}
</WithLocale>
)}
</WithTheme>
)}
</WithUser>
```
2. React DevTools 雜訊增加
每個邏輯共用元件都會出現在 DevTools 的 component tree 中,影響閱讀與除錯體驗。
### Hooks 取代 Render Props 的方式
React Hooks 問世後,render props 的用途大幅被取代。使用 Custom Hook 可以更簡潔達成相同邏輯重用。
### Custom Hook 範例
```jsx
// 共用邏輯抽出來
function useUser() {
const user = { name: 'Yo0', age: 25 };
return user;
}
function App() {
const user = useUser(); // 直接取得資料
return <div>Hello, {user.name}</div>;
}
```
### Render Props vs Hook 比較
| 特性 | Render Props | Hooks |
| -------------------- | --------------------- | ------------------------------ |
| 引入方式 | Component | Function (`useXxx`) |
| 共用邏輯可讀性 | 中等(callback 巢狀) | 高(線性執行、邏輯清晰) |
| 呈現在 component tree 中 | 是(有額外元件層) | 否(不增加渲染層) |
| 依賴 React 版本 | Class / Function 元件皆可 | 需搭配 Function Component + 16.8+ |
| 命名衝突 | 不會 | 不會 |
Render Props 是在 Hooks 出現前很常見的邏輯抽象模式,儘管現在較少使用,但在某些場景中仍然適用,如:
* 必須支援 **Class Component**
* 想要將 **UI 控制完全交由使用者決定**
* 需要在 **複雜 UI 動態組合**時維持高度彈性
## 活動後 QA 重點整理
對於資歷 1~3 年的前端工程師來說,最重要的並不是深入本科系那套硬派的資料結構與演算法(雖然這些在某些階段依然重要),而是應該**把前端基礎打好、打深**。這裡的「基礎」指的是像 JavaScript 的核心觀念、瀏覽器行為等,能夠真正理解並應用的知識。
> 什麼是 event loop?什麼是 closure?
> 當我們在 React 中呼叫 `setState`,直到瀏覽器畫面實際渲染產生變化,中間經歷了哪些階段?
這些問題並非單純的知識問答,而是幫助我們在實作中做出更好判斷的重要工具。
### 專案開發時的自我檢視
在日常開發過程中, Zet 建議可以帶著以下幾個問題來思考,作為提升的方向:
1. **我們是否能用更好的方式來實作?**
2. **這次的經驗,有沒有內化成為自己能再次使用的知識?**
3. **在選擇某項技術或架構時,是否有衡量過使用的利弊?**
4. **時間壓力不應成為阻礙思考的理由。**
即使這次沒能做到理想實作,是否能為下次遇到同類問題做更好的準備?
### React 資料夾結構與狀態管理的思維
1. 沒有絕對標準答案。資料夾架構的設計,通常依據專案需求與團隊習慣調整,沒有唯一正解。
2. 實務上常見的做法是採用功能切分(feature-based),也就是依照模組或功能面向來組織元件。這樣能讓維護與擴充更直覺。可以用 component unit 的思維作為切入點:每個單元有明確的語意與責任範圍。
3. 狀態管理方面,過早引入外部套件(如 Redux、Zustand)並非必要。實際上,多數情境可以透過 React 內建的 state、context 等機制完成。建議只有在以下情況再考慮使用外部工具:
- 跨模組或跨頁面需要共享狀態(例如用戶登入資訊、購物車等)
- 狀態變化頻繁且需要細緻的效能優化(如避免不必要的 re-render)
- 特殊資料來源的快取策略,像是需要與伺服器密切同步的資料(例如使用 Apollo Client 的 useQuery 時,GraphQL 查詢資料的快取邏輯就較複雜,這時會需要專門的工具來協助)
### 元件拆分的思考方式
設計元件時可以從「**邊界的定義**」來思考:
* `ProductList` 是一個與「商品」概念密切相關的元件,具備清楚語意與職責。
* 相對地,`List` 是一個通用列表容器,並不具有特定領域語意。
透過這樣的語意區分,可以更有效地拆分出 **高內聚、低耦合** 的元件,提升可維護性與可讀性。
### 理論與實作的平衡
* **理解程式碼的實際影響力**,是前端工程師重要的基礎能力。
* 實作與理論不應脫鉤:不是先「背完理論」才動手,也不是「寫完程式」才來補概念。
* 最理想的方式是:**小步快跑、邊做邊學**,從實作中驗證理論,從理論中優化實作。