Skip to main content

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>
);
};
}