# [筆記] 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` 去處理 - 講義練習: ![image](https://hackmd.io/_uploads/Syob2C3Lex.png) --- ## 依賴注入 - `(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} /> )} /> ```