Hook 時代 HoC 還有哪些優勢 ?
在 Hook 時代,狀態邏輯處理的抽取複用變得簡單易用,但這就代表 HoC 沒有出場機會嗎?
我們可以藉由 HoC 與 Hook 的核心差異來思考哪些場景要派誰上場
HoC 的核心精神 : 一個包裝器,用函數將「整個元件」包裹起來,邏輯處理後將資料藉由 「注入」被包裹的元件的 prop
Hook 的核心精神:把 Hook 系列的狀態管理與運算邏輯封裝起來,在「元件內部」輕鬆複用
這是我們可以看到一個差異點
「整個元件」 vs.「元件內部」
從這個角度思考,就可以看出兩個模式適用的場景,接著來看看兩個模式的 pseudo code
HoC
function withMyHoc(WrappedComponent, ...params) {
// 返回一個包裝後的新元件
return function WithMyHoc(props) {
// do some logic (especially state)
// const isAuth = ...
// const result = ...
if (isAuth) {
<WrappedComponent result={result} />;
return;
}
return <ErrorPage />;
};
}
// 使用 HoC 來增強原先的元件, 返回一個新元件
const MyHocComponent = withMyHoc(MyComponent, ...)
Hook
function useMyHook() {
// do some logic (especially state)
// const isAuth = ...
// const result = ...
return [isAuth, result];
}
// before
function MyComponent(props) {
const [x, y] = useMouse();
return (
<div>
{x} {y}
</div>
);
}
// after adding useMyHook
function MyComponent(props) {
const [isAuth, result] = useMyHook();
const [x, y] = useMouse();
if (isAuth) {
return (
<div>
{x} {y}
</div>
);
}
return <ErrorPage />;
}
HoC 的專長
從這兩個例子,我們可以發現 :
HoC 擅長對元件進行「整體性」改造
- 條件性渲染(HoC 最主要的用途)這種時候用在權限管理特別有用,經過 HoC 封裝,就可以動態決定要渲染被包裹的元件還是錯誤顯示
- 在 Hook 範例可以看到,Hook 使用是在「元件內部」,如果原先沒有設計任何權限相關的條件式渲染,新加入一個 hook 後,我們需要在元件內進行 破壞性 的修改 (Breaking changes),來達到條件渲染
- 但是在 HoC,幾乎不需要動到內部元件,藉由外層包裝的判斷來進行條件渲染
- 或者 theme 主題樣式、大範圍的 Feature Flags
- 想一次在多個元件快速套用共享邏輯,HoC 也是個好選擇,只要呼叫
withHoC(YouComponent)
即可,而 Hook 需要在元件內部裡面一個一個加入
如果複用型的元件的 props 彈性做得比較大,HoC 可以輕鬆地封裝這些元件來進行「加料」
還有一些情況可能是 Hook 無法做到的 :
- Error Boundaries
- 兼容 Class Component 的 legacy code
Hook
Hook 比較像是方便複用共同運算邏輯的小工具,可以在元件內部使用,讓語法更加簡潔
什麼是共同運算邏輯的小工具?我的認知是,元件「內部細節」使用到的運算邏輯,而非關連整體元件的處理,我們可以看看 useHooks – The React Hooks Library 就可以一窺 Hook 在什麼
- useMouse : 輕鬆取得滑鼠座標
- useWindowSize : 輕鬆取得瀏覽器視窗長寬
- react-router 的 useLocation : 取得網址列
可以發現 custom hook 可以把元件頻繁碰到的實現細節抽取出來,變得簡單易用
結論
HoC 使用場景
當需要控制元件的條件渲染邏輯或注入額外的 prop 時,HoC 是較佳選擇(例如權限管理),而且 HoC 更適合快速在多個元件套用共享邏輯(只需要包裝起來然後呼叫 HoC)
Hook 使用場景
Hook 提供了一種更簡潔和直觀的方式來處理元件內部的狀態和生命週期,當需要在元件「內部」複用實現細節相關的狀態邏輯或 effect 時,使用 Hook 更合適
附錄 : HoC 也可以在元件內部使用 (待商榷)
在組裝多個小型元件的複合元件,也可以在內部使用 HoC 來進行組裝,不一定要馬上 export default withHoc(MyComponent)
但如果在元件裡面使用 HoC 的話,每次父元件 re-render 都會重新建立子元件,這時可以需要使用 useMemo
固定住,再選配使用 React.memo
,但放在元件內使用 HoC 可能很特殊情況,需要動態使用 HoC 可能才會用到,
否則把 HoC 的呼叫建立,放在父元件函數外就可以避免不斷重複建立元件
function Form(props) {
// 避免 Form 每次 re-render 時要重新建立 PermissionButton
const PermissionButton = useMemo(() => {
return memo(withAuth(Button));
}, []);
return (
<div>
<Button>一般人可以看到</Button>
<PermissionButton>課金專用</PermissionButton>
</div>
);
}
兩種 HoC 設計方法
withHoc(param)(Component)
vs. withHoc (Component, params)
第一個使用了 Currying,第一次調用 param 會回傳一個「回傳元件的函式」,第二次調用才是把元件包裹進去
好處是配置靈活,像是用不同參數生成不同 HoC
const withColor = (color) => (Component) => {
return (props) => (
<div style={{ backgroundColor: color }}>
<Component {...props} />
</div>
);
};
// 原始元件
const MyComponent = () => <div>這是我的組件</div>;
// 應用 Hoc
const withBlue = withColor('lightblue')
const withRed = withColor('red')
const MyBlueComponent = withBlue(MyComponent);
const MyRedComponent = withRed(MyComponent);
// or
const MyBlueComponent = withColor('lightblue')(MyComponent)
第二個方法比較直觀,就是回傳包裝後的元件
const withColor = (Component, color) => {
return (props) => (
<div style={{ backgroundColor: color }}>
<Component {...props} />
</div>
);
};
// 原始元件
const MyComponent = () => <div>這是我的組件</div>;
// 應用 Hoc
const ColoredMyComponent = withColor(MyComponent, 'lightblue');
附錄 : Feature Flag 是什麼
通俗点讲,Feature flags 是一种将 " 功能开关 + 灰度发布 + 远程配置 + ab 测试 + 版本控制 + 持续交付 + 订阅管理 + 等等 " 多个能力融为一体的技术。
Reference : 关于Feature flags - 知乎
附錄 : Error Boundaries 是什麼
Reference : Error Boundaries – React
Error Boundary 是利用 React 生命週期方法來捕捉錯誤的元件,錯誤發生時可以改為渲染 fallback UI,Hook 原生無法實現這個功能,得靠 Class Component
另一方面,try...catch
可以捕捉函數元件內部 code block 邏輯處理錯誤,但是碰到 元件整體性的錯誤 還是得使用 Error Boundaries 來捕捉,例如渲染過程的錯誤、生命週期方法的錯誤
// ErrorBoundary 元件要用 Class Component 的生命週期來做
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export function withErrorBoundary(WrappedComponent) {
return function(props) {
return (
<ErrorBoundary>
<WrappedComponent {...props} />
</ErrorBoundary>
);
};
}