Skip to content

Instantly share code, notes, and snippets.

@riskers riskers/HOC.md
Last active May 27, 2019

Embed
What would you like to do?
react HOC / render props / hook

HOC

高阶组件其实并不是组件,只是一个函数而已。它接收一个组件作为参数,返回一个新的组件。我们可以在新的组件中做一些功能增加,渲染原有的组件。这样返回的组件增强了功能,但渲染与原有保持一致,没有破坏原有组件的逻辑。

因此在提取不同类别组件相似的行为时,高阶组件是非常合适的选择。举例说明的话,组件异步加载、异步加载 script 后显示组件、数据源绑定、拖拽排序。

render props

去渲染一个父子组件,但是子组件依赖于父组件的某些数据: 可以在父组件中做一些通用性的逻辑,并将数据抛给子组件。子组件可以任意渲染成自己想要的样子。比如说我们可以在父组件中做一个倒计时的逻辑,然后把倒计时的时间传给子组件,这样子组件任意渲染成什么样都可以,子组件只自己知道会定时的拿到新的时间而已。

hooks

react hooks 的意义是:

  • 代替 render props 这种 HOC 方式复用逻辑
  • 代替生命周期函数,不再将逻辑散落在生命周期函数里
  • 更加函数式,没有 class 也就少了副作用

看到 useEffect ,我想了想我对于副作用的理解:UI = F(Props) ,一个组件最终的dom结构与样式是由父级传递的props决定的。

「纯函数」:意思是固定的输入必然有固定的输出,它不依赖任何外部因素,也不会对外部环境产生影响。

react希望自己的组件渲染也是个纯函数,所以有了纯函数组件。然而真正的业务场景是有各种状态的,实际影响UI的还有内部的state。(其实还有context,暂时先不讨论)。

UI = F(props, state, context)

这个state可能会因为各种原因产生变化,从而导致组件的渲染结果不一致。相同的入参(props)下,每次render都有可能返回不同的UI。因此任何导致此现象的行为都是副作用(side effects)。比如用户点击下一页,导致页码与列表发生变化,这就是副作用。同样的props,不点击时是第一页数据,点击一下后,变成了第二页的数据or请求失败的页面or其他UI交互。

当然state是明面上影响了UI,暗地里,可能还有其他因素会影响UI。比如比如网络请求、操作 DOM等。

高阶组件:接收函数作为输入,或者输出另一个函数的一类函数,被称作高阶函数。对于高阶组件,它描述的便是接受React组件作为输入,输出一个新的React组件的组件。即高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件,供其他组件调用。

什么时候使用高阶组件:在React开发过程中,发现有很多情况下,组件需要被"增强",比如说给组件添加或者修改一些特定的props,一些权限的管理,或者一些其他的优化之类的。而如果这个功能是针对多个组件的,同时每一个组件都写一套相同的代码,明显显得不是很明智,所以就可以考虑使用HOC。

react-redux 的 connect 方法就是一个 HOC ,他获取 wrappedComponent ,在 connect 中给 wrappedComponent 添加需要的 props。

最基本的一个 HOC

// define
const withTitle = WrappedComponent => class extends React.Component {
  render() {
    return (
      <WrappedComponent
        {...this.props}
       />
     )
  }
}
// use
@withTitle
class Demo extends React.Component {
  render() {
    // ...
  }
}

上面语法等同于 const EnhanceDemo = withTitle(Demo)

如果 Demo 是 stateless 组件,则只能 const EnhanceDemo = withTitle(Demo) 这种写法

debug

因为 withTitle 返回一个匿名组件,所以在调试的时候:

可以加一个 displayName 解决:

const withTitle = WrappedComponent => class extends React.Component {
  static displayName = `HOC(${WrappedComent.displayName})`
  render() {
    return (
      <WrappedComponent
        {...this.props}
       />
     )
  }
}

组件参数

const withTitle = title => WrappedComponent => class extends React.Component {
  render() {
    return <div>
      <div>
        { title || '我是标题'}
      </div>
      <WrappedComponent {...this.props}/>
    </div>
  }
}


@withTitle('test title')
class A extends React.Component {
  constructor(props){
    super(props)
  }
  render(){
    return(
      <h3>{this.props.children}</h3>
    )
  }
}

HOC 与 父组件的不同

相对 HOC 来说,父组件可以做什么,不可以做什么?我们详细地总结一下:

  • 渲染劫持 (在 Inheritance Inversion 一节讲到)
  • 操作内部 props (在 Inheritance Inversion 一节讲到)
  • 提取 state。但也有它的不足。只有在显式地为它创建钩子函数后,你才能从父组件外面访问到它的 props。这给它增添了一些不必要的限制。
  • 用新的 React 组件包裹。这可能是唯一一种父组件比 HOC 好用的情况。HOC 也可以做到。
  • 操作子组件会有一些陷阱。例如,当子组件没有单一的根节点时,你得添加一个额外的元素包裹所有的子组件,这让你的代码有些繁琐。在 HOC 中单一的根节点会由 React/JSX语法来确保。
  • 父组件可以自由应用到组件树中,不像 HOC 那样需要给每个组件创建一个类。

一般来讲,可以用父组件的时候就用父组件,它不像 HOC 那么 hacky,但也失去了 HOC 可以提供的灵活性。

Inheritance Inversion

之所以被称为 Inheritance Inversion 就是因为 WrappedComponent 被 Hoc 继承了,而不是 WrappedComponent 继承 Hoc。

// II 最简实现
function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

Inheritance Inversion 允许 HOC 通过 this 访问到 WrappedComponent,意味着它可以访问到 state、props、组件生命周期方法和 render 方法。

至于为什么能够继承 WrappedComponent 的 state、props、lifecycle、render,详见 es2015 的 class 语法

II 作用

  • 渲染劫持
  • 操作 state

渲染劫持

HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事

通过渲染劫持你可以:

  • 在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
  • 读取和修改由 render 输出的 React 元素树
  • 有条件地渲染元素树
  • 把样式包裹进元素树(就像在 Props Proxy 中的那样)
// demo1: 条件渲染
// 当 this.props.loggedIn 为 true 时,这个 HOC 会完全渲染 WrappedComponent 的渲染结果。(假设 HOC 接收到了 loggedIn 这个 prop)
function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}
// demo2: 修改由 render 方法输出的 React 组件树
// 如果 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”
function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      const elementsTree = super.render()
      let newProps = {};
      if (elementsTree && elementsTree.type === 'input') {
        newProps = {value: 'may the force be with you'}
      }
      const props = Object.assign({}, elementsTree.props, newProps)
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
      return newElementsTree
    }
  }
}

操作 state

HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。

export function IIHOCDEBUGGER(WrappedComponent) {
  return class II extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
          
          <p>这里的 this.state 竟然是 WrappedComponent 的 state</p>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}

Props Proxy

hoc 有两种实现方式:

  • Props Proxy(PP): HOC 操作 WrappedComponent 的 props
  • Inheritance Inversion(II): HOC 继承 WrappedComponent
// PP 最简实现
function ppHOC(WrappedComponent) {  
  return class PP extends React.Component {    
    render() {      
      return <WrappedComponent {...this.props}/>    
    }  
  } 
}

PP 作用

  • 操作 props
  • 通过 refs 访问组件实例
  • 提取 state
  • 用其他元素包裹 WrappedComponent

操作 props

可以读取、添加、编辑、删除传给 WrappedComponent 的 props。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

通过 Refs 访问到组件实例

function PPHOC(WrappedComponent) {
  return class PP extends React.Component {
    
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.xxx()  //  调用组件实例的 xxx 方法
    }

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})  // 合并 props
      return (
        <div>
          <WrappedComponent {...props} />
        </div>
      )
    }
  }
}

@PPHOC
class Example extends React.Component {
  constructor(props) {
    super(props)
  }
  xxx () {
    console.log('this is xxx method')
  }
  render() {
    return (
      <div>
        <h2>
          Wrapped Component
        </h2>
      </div>
    )
  }
}

提取 state

通过传入 props 和回调函数把 state 提取出来

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

@ppHOC
class Example extends React.Component {
  render() {
    return <input name="name" {...this.props.name}/>
  }
}

这个 input 会自动成为受控input

用其他元素包裹 WrappedComponent

用来封装样式、布局等

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
}

与 props render 和 hoc 作用一样,用于复用业务逻辑,但是比他们简单,也更函数式

hooks 和 redux 没有关系,redux 是状态管理,处理组件之间共享状态的问题,而 hooks 处理的是复用状态逻辑的问题。


前文比较了定时器需求中的 useState、useEffect、useReducer 和 useRef 的四种实现方式,正好遍历了主要的 React Hooks API。后文讲述了另一个定时器需求,比较了 useEffect、useLayoutEffect 和 useReducer 三种实现,解释了为何异步的 useEffect 会导致闭包变量读取问题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.