Published on

React Component Design - Higher-Order Component

Authors
  • avatar
    Name
    Alex Yu
    Twitter

Higher-Order Component的簡稱叫HOC,設計概念主要是要把一些共用的邏輯抽取出來,讓其他元件透過HOC的方式能獲得這些共用邏輯的功能,讓元件的使用方式不一定要侷限在這個HOC

HOC的概念很重要的一點是利用composition的方式來產生一個新的component,而不是直接mutate傳入的component

我們拿之前介紹過的Provider來改寫成HOC試試

import React, {Fragment} from 'react'
// copy static methods
import hoistNonReactStatics from 'hoist-non-react-statics'
import { Switch } from '../switch'

const ToggleContext = React.createContext()

class Toggle extends React.Component {
  static Consumer = ToggleContext.Consumer

  toggle = () =>
    this.setState(
      ({ on }) => ({ on: !on }),
      () => this.props.onToggle(this.state.on),
    )

  state = { on: false, toggle: this.toggle }

  render() {
    return (
      <ToggleContext.Provider value={this.state} {...this.props} />
    )
  }
}

function withToggle(Component) {
  function Wrapper(props, ref) {
    return (
      <Toggle.Consumer>
        {toggleContext => (
          <Component toggle={toggleContext} {...props} ref={ref} />
        )}
      </Toggle.Consumer>
    )
  }

  Wrapper.displayName = `withToggle(${Component.displayName ||
    Component.name})`

  return hoistNonReactStatics(React.forwardRef(Wrapper), Component)
}

const Layer1 = () => <Layer2 />
const Layer2 = withToggle(({ toggle: { on }}) => (
  <Fragment>
    {on ? 'The button is on' : 'The button is off'}
    <Layer3 />
  </Fragment>
))
const Layer3 = () => <Layer4 />
const Layer4 = withToggle(({ toggle: { on, toggle }}) => (
  <Switch on={on} onClick={toggle} />
))

function Usage({
  onToggle = (...args) => console.log('onToggle', ...args),
}) {
  return (
    <Toggle onToggle={onToggle}>
      <Layer1 />
    </Toggle>
  )
}

讓我們來看看HOC幾個要注意的地方

  • displayName需要設定
  • 傳入的componentstatic methods需要copy到HOC裡面新的component,所以一般會透過hoist-non-react-statics複製static methods到新的component
  • 如果有希望使用ref,需要透過React.forwardRef的方式轉換

從上面幾點不難看出HOC有滿多的缺點,而且在現在React 18的環境中,其實已經有一些方式可以替代HOC,比如之前介紹過的Render Propspattern,或者是hook的方式。

讓我們先把上面的例子改寫成Render Props的形式來看看

class ToggleWrapper extends React.Component {
  render() {
		return (
      <Toggle.Consumer>
        {toggleContext => (
					this.childen({ toggle: toggleContext })
        )}
      </Toggle.Consumer>
    )
  }
}

Render Props這樣的方式看起來差別不大,但優點其實很多

  • children不受限制
  • 純粹是一個React Component,讓React tree乾淨
  • 可以寫Prop Types檢驗props了!
  • static methodsref的問題都同時解決了
  • props不用spred

Reference