# [筆記] TBC-【Zet】 React Render Props
:::info
這篇是參加「Tech Book Community 線下小聚 in 台北 - 【Zet】 React Render Props」場次的小小筆記,聽了之後發現自己偶爾也會用到這些寫法,只是不知道有 pattern 可以 follow,而且途中還有安排給大家練習程式碼的時間,加深了不少印象。
感謝 Zet 的分享,也很感謝 Lois 舉辦活動,甚至還有分享心得的抽書活動,否則我還真的不知道會拖到什麼時候才來整理筆記,真的太感謝惹。
:::
:::success
- 【活動】:{%preview https://tech-book-community.kktix.cc/events/2025-07 %}
- 【共筆】:{%preview https://hackmd.io/@tech-book-community/r1j_Z5vUgg %}
- 【講義】:{%preview https://zet-chou.notion.site/TBC-React-render-props-design-pattern-1d1a3f5665b080ddbcacf9e320854211 %}
:::
---
[TOC]
---
## Facade Pattern
- 豆知識:`Facade` 這個單字源自於法文,所以要念作 `/fəˈsɑːd/`,有「正面」、「門面」、「外觀」的含義在
- 解決什麼問題:
- 當我們不想在使用某個功能的時候,還得手動一一呼叫所有複雜的子系統
- 主要是提供一個介面,隱藏了內部的複雜性
- 就像是去咖啡廳點餐,只要跟店員說「一杯拿鐵」,而不用親自去操作磨豆機、咖啡機、奶泡機等等
- 【講義範例】:MediaPlayer, React useUser custom hooks
### 其他範例:操作 `localStorage`
> 底下是跟 `Gemini` 與 `Claude` 產出來的
- 原本寫法:
```javascript!
const userSettings = {
theme: 'dark',
notifications: true,
language: 'zh-TW'
};
// 【儲存】需要 2 步操作
const settingsString = JSON.stringify(userSettings);
localStorage.setItem('userSettings', settingsString);
// --- 在別的地方或下次載入頁面時 ---
// 【讀取】需要 3 步操作,包含錯誤處理
const savedString = localStorage.getItem('userSettings');
// 預設值
let loadedSettings = {};
if (savedString) {
loadedSettings = JSON.parse(savedString);
}
// 'dark'
console.log(loadedSettings.theme);
```
- `BrowserStorage`:
```javascript!
const BrowserStorage = {
save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.warn(`儲存失敗: ${key}`, error.message);
return false;
}
},
get(key, defaultValue = null) {
try {
const savedValue = localStorage.getItem(key);
if (savedValue !== null) {
return JSON.parse(savedValue);
}
return defaultValue;
} catch (error) {
console.warn(`讀取失敗: ${key}`, error.message);
return defaultValue;
}
}
};
```
- 使用方式:
```javascript!
BrowserStorage.save('userSettings', userSettings);
```
```javascript!
BrowserStorage.get('broken', '預設值');
```
---
## 控制反轉
- `(Inversion of Control, IOC)`
- 過往都是由「主程式」去呼叫 API,而 `IOC` 則是主程式只描述「**希望達成什麼效果**」,並將執行的「**邏輯細節**」交出去
- 【講義範例】: `for` VS `array.map()`
- `for` 迴圈:自己控制流程,包含索引、條件、終止
- `array.map()`:提供了轉換的函式(想達成的效果),而計數器的控制權「**反轉**」給 `map()` 方法
### 不一定要有 callback 才算是控制反轉
- `SQL` 查詢交給「資料庫引擎」掌控流程
- `CSS` 樣式交給「瀏覽器的 `CSSOM`」
- 對應 DOM element 的 `React element` 交給「`React DOM Reconciler` & `Renderer`」
### React Reconciler
- `JavaScript` 是單執行緒,如果龐大的元件樹在 `render` 的時候一次跑完、不可中斷,就會導致畫面卡頓
- `React` 透過 `IOC` ,讓 `render` 的過程可以被切斷,所以我們只需要説希望畫面長什麼樣子,剩下的細節就交給 `React` 去處理
- 講義練習:

---
## 依賴注入
- `(Dependency Injection, DI)`
- 如果說 `IOC` 是目標,那 `DI` 就是達成目標的手段
- 方便替換邏輯
- 不用寫死
- 好測試
- 【講義範例】:`handleClick` 傳給 `Button` 元件,再去跑 `onClick`
- 把 `handleClick` 函式,透過 `onClick` `prop` **傳遞進去**的過程是依賴注入
- 點選 `onClick` 時的執行時機是控制反轉
---
## Render props
- 在 `Custom Hooks` 出現之前,`Render Props` 的主要用途是解決有狀態邏輯 (Stateful Logic) 的共用問題
- 而目前在純邏輯共用的場景,`Render Props` 的地位已被 `Custom Hooks` 取代掉了
### Component UI 的抽象化設計用途
- UI 靈活性與組合
- 把該寫死的地方寫死,同時為需要彈性的部分開孔出去
- 希望外層元件可以拿到底下元件的內部狀態或資料,來客製化部分的 UI 顯示,但又不想把狀態提升到外層的時候
- 【講義範例】:
- `ProductList` 接收 `items` 來 `render` 列表
- `DiscountProductList` 沿用 `ProductList` 並注入客製化邏輯的「特殊版本」來顯示特價
### 其他範例:按鈕開關對話視窗
> 這是公司前輩寫的元件,覺得滿方便的,所以筆記起來
```typescript!
import { useState } from 'react'
type Props = {
renderButton: (props: { open: boolean; openDialog: () => void; toggleDialog: () => void }) => React.ReactNode
renderDialog: (props: { open: boolean; closeDialog: () => void; toggleDialog: () => void }) => React.ReactNode
}
export default function DialogButtonWrapper(props: Props) {
const [open, setOpen] = useState<boolean>(false)
const openDialog = () => {
setOpen(true)
}
const closeDialog = () => {
setOpen(false)
}
const toggleDialog = () => {
setOpen(p => !p)
}
return (
<>
{props.renderButton({ open, openDialog, toggleDialog })}
{open && props.renderDialog({ open, closeDialog, toggleDialog })}
</>
)
}
```
- 使用方式
```typescript!
<DialogButtonWrapper
renderButton={({ openDialog }) => (
<Button variant='outlined' onClick={openDialog}>
{t('common.edit')}
</Button>
)}
renderDialog={({ open, closeDialog }) => (
<MemberPointFormDialog
id={id}
open={open}
onClose={closeDialog}
/>
)}
/>
```