# 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` 是一個通用列表容器,並不具有特定領域語意。 透過這樣的語意區分,可以更有效地拆分出 **高內聚、低耦合** 的元件,提升可維護性與可讀性。 ### 理論與實作的平衡 * **理解程式碼的實際影響力**,是前端工程師重要的基礎能力。 * 實作與理論不應脫鉤:不是先「背完理論」才動手,也不是「寫完程式」才來補概念。 * 最理想的方式是:**小步快跑、邊做邊學**,從實作中驗證理論,從理論中優化實作。