---
# System prepended metadata

title: TBC 線下技術小聚：React Render Props Design Pattern
tags: [Design Pattern, React]

---

# 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` 是一個通用列表容器，並不具有特定領域語意。

透過這樣的語意區分，可以更有效地拆分出 **高內聚、低耦合** 的元件，提升可維護性與可讀性。

### 理論與實作的平衡

* **理解程式碼的實際影響力**，是前端工程師重要的基礎能力。
* 實作與理論不應脫鉤：不是先「背完理論」才動手，也不是「寫完程式」才來補概念。
* 最理想的方式是：**小步快跑、邊做邊學**，從實作中驗證理論，從理論中優化實作。
