Skip to content

Instantly share code, notes, and snippets.

@riskers
Last active January 12, 2025 06:59
Show Gist options
  • Select an option

  • Save riskers/d1b4d81f3b615216364508067b9c0327 to your computer and use it in GitHub Desktop.

Select an option

Save riskers/d1b4d81f3b615216364508067b9c0327 to your computer and use it in GitHub Desktop.
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等。

hooks vs class

现在 hooks 让函数式组件完全可以代替 class 组件,用了 hooks 的函数组件可以很明显地看到两部分:数据处理和 UI,相比 class 组件中的混乱声明周期更高明一点。

高阶组件:接收函数作为输入,或者输出另一个函数的一类函数,被称作高阶函数。对于高阶组件,它描述的便是接受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>
      )
    }
  }
}

demo

hooks 作用:

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

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

  1. function component + hooks 能够代替 class component,因为 hooks 能够让 function component 有 state

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

@riskers

riskers commented Sep 21, 2020

Copy link
Copy Markdown
Author

逻辑复用

image

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment