HoC、Render Props、Custom Hook 缺點比對
前言
HoC、Render Props、Custom Hook 都想做到的事情 : state 與資料處理的瑣碎邏輯,能抽象化出來複用
Hoc 缺點
reference
以下是對這篇文章的一點小整理,我覺得說得都有道理,但這些都是比較極端的情境
props 容易打架
我們將邏輯封裝在 hoc,再把要加入的狀態 以 prop 傳入原有的元件
但是如果有巢狀使用多個 Hoc,很難看得出來同一個 prop 名稱會不會在這多個 hoc 中重複使用,這個問題也不太好 debug
export default someHoC(anotherHoc(coolHoc(MyComponent)))
不能複用同個 HoC 兩次以上
既然有上述提到的問題,同一個元件無法使用 hoc 兩次(例如:這個元件想要兩個地方開啟 toggle 功能)
export default withToggle(withToggle(MyComponent))
Render Props 或 Custom Hooks 可解決,因為可以針對元件內個別需要的元件進行複用
多個 HoC 下,無法明顯看出哪些 props 來自哪個 HoC
如果包裝多個 hoc,一個元件會「隱性地」接受到很多 prop,很難分得清楚哪些 prop 來自於誰,又有哪些是該元件原生的 prop
function MyComponent({ name, onClick, setValue, time, date, isActive, isRemoved }) {
// ...
}
export default someHoC(anotherHoc(coolHoc(MyComponent)))
Dev Tool 會有 with...
的巢狀結構
<withA>
<withB>
<withC>
<MyComponent />
</withC>
</withB>
</withA>
HoC 缺點小結論
- 根據 Hook 時代 HoC 還有哪些優勢 ,目前在使用 HoC 的情景也不會包太多層,甚至一層就可以解決,這些缺點應該是稍微注意即可
Render Props 缺點
reference
學習曲線略高、程式碼醜又不直觀
Render Props 可以解決 HoC 的缺點(雖然現在來看兩者適用情境本來就不同),而且變得十分自由彈性,但是程式碼會變得難以理解
來自 react-beautiful-dnd
的例子
import { Droppable } from 'react-beautiful-dnd';
<Droppable droppableId="droppable-1" type="PERSON">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
{...provided.droppableProps}
>
<h2>I am a droppable!</h2>
{provided.placeholder}
</div>
)}
</Droppable>;
來自 react-table
v7 的例子 (不是 Render Props,但也是傳遞一個渲染元件的 function)
{
Header: "Enable",
id: "enable",
accessor: "enable",
Cell: ({ value, row }) => (
<FormCheck checked={value} onChange={(e) => handleCheckChange(row.index, e)} />
),
},
不過就我的觀察,會使用 Render Prop 的套件通常都想要做到容易複用,但用得保留一定的自由度和彈性,例如 Drag & Drop 和 Table 元件都是需要客製化程度很高的場景
所以使用 Render Prop 作為設計模式也是合理的選擇
Dev Tool 也會有巢狀結構 (但是沒 HoC 那麼嚴重)
Custom Hooks 缺點
reference
Re-Render 效能議題
父元件中使用 custom hook 並 re-render,代表全部子元件也會 re-render
但是如果父元件非使用 custom hook,只對需要的子元件使用 Render Props 封裝,邏輯如果觸發 re-render,只會發生在 Render Props 元件內部,並只造成那個子元件 re-render
以下範例取自 Reference 的程式碼
Custom Hook 版本
在表單部分不斷使用 setValue
打字時,Page
會不斷進行 re-render
若子元件沒有使用 React.memo
,父元件 re-render 也會造成底下子元件 re-render,所以單單因為一個表單 Input 不斷更新,會不斷造成其他子元件全部都要 re-render
function Page() {
const { values, setValue } = useForm();
return (
<>
<Header />
<Navigation />
<SomeOtherThirdPartyComponent />
<form>
<input
value={values.name}
onChange={e => {
setValue("name", e.target.value);
}}
/>
<input
value={values.email}
onChange={e => {
setValue("email", e.target.value);
}}
/>
</form>
<Footer />
</>
);
}
Render Props 版本
雖然醜了一點,但是 values
和 setValues
都是在 FormManager
元件內部,再傳給 children
所以在 form
的 input
不斷輸入的時候,只會觸發 FormManager
內部 re-render,不會造成其他 Header
、Navigation
等元件 re-render
function Page() {
return (
<>
<Header />
<Navigation />
<SomeOtherThirdPartyComponent />
<FormManager>
{({ values, setValue }) => (
<form>
<input
value={values.name}
onChange={e => {
setValue("name", e.target.value);
}}
/>
<input
value={values.email}
onChange={e => {
setValue("email", e.target.value);
}}
/>
</form>
)}
</FormManager>
<Footer />
</>
);