React高级组件精讲
高阶函数是以函数为参数,并且返回也是函数的的函数。类似的,高阶组件(简称HOC)接收 React 组件为参数,并且返回一个新的React组件。高阶组件本质也是一个函数,并不是一个组件。高阶组件的函数形式如下:
const EnhanceComponent = higherOrderComponent(WrappedComponent)
通过一个简单的例子解释高阶组件是如何复用的。现在有一个组件MyComponent,需要从LocalStorage中获取数据,然后渲染到界面。一般情况下,我们可以这样实现: import React, { Component } from 'react' class MyComponent extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { return( <div>{this.state.data}</div> ) } }
代码很简单,但当其它组件也需要从LocalStorage 中获取同样的数据展示出来时,每个组件都需要重写一次 componentWillMount 中的代码,这显然是很冗余的。下面让我人来看看使用高阶组件改写这部分代码。
import React, { Component } from 'react' function withPersistentData(WrappedComponent) { return class extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件 return <WrappedComponent data={this.state.data} {...this.props}/> } } } class MyComponent extends Component{ render() { return <div>{this.props.data}</div> } } const MyComponentWithPersistentData = withPersistentData(MyComponent);
withPersistentData 就是一个高阶组件,它返回一个新的组件,在新组件中 componentWillMount 中统一处理从 LocalStorage 中获取数据逻辑,然后将获取到的数据通过 props 传递给被包装的组件 WrappedComponent,这样在WrappedComponent中就可以直接使用 this.props.data 获取需要展示的数据,当有其他的组件也需要这段逻辑时,继续使用 withPersistentData 这个高阶组件包装这些组件。
二、使用场景
高阶组件的使用场景主要有以下4中:
操纵 props
通过 ref 访问组件实例
组件状态提升
用其他元素包装组件
1.操纵 props
在被包装组件接收 props 前, 高阶组件可以先拦截到 props, 对 props 执行增加、删除或修改的操作,然后将处理后的 props 再传递被包装组件,一中的例子就是属于这种情况。
2.通过 ref 访问组件实例
高阶组件 ref 获取被包装组件实例的引用,然后高阶组件就具备了直接操作被包装组件的属性或方法的能力。
import React, { Component } from 'react' function withRef(wrappedComponent) { return class extends Component{ constructor(props) { super(props); this.someMethod = this.someMethod.bind(this); } someMethod() { this.wrappedInstance.comeMethodInWrappedComponent(); } render() { // 为被包装组件添加 ref 属性,从而获取组件实例并赋值给 this.wrappedInstance return <wrappedComponent ref={(instance) => { this.wrappedInstance = instance }} {...this.props}/> } } }
当 wrappedComponent 被渲染时,执行 ref 的回调函数,高阶组件通过 this.wrappedInstance 保存 wrappedComponent 实例引用,在 someMethod 中通过 this.wrappedInstance 调用 wrappedComponent 中的方法。这种用法在实际项目中很少会被用到,但当高阶组件封装的复用逻辑需要被包装组件的方法或属性的协同支持时,这种用法就有了用武之地。
3.组件状态提升
高阶组件可以通过将被包装组件的状态及相应的状态处理方法提升到高阶组件自身内部实现被包装组件的无状态化。一个典型的场景是,利用高阶组件将原本受控组件需要自己维护的状态统一提升到高阶组件中。
import React, { Component } from 'react' function withRef(wrappedComponent) { return class extends Component{ constructor(props) { super(props); this.state = { value: '' } this.handleValueChange = this.handleValueChange.bind(this); } handleValueChange(event) { this.this.setState({ value: event.EventTarget.value }) } render() { // newProps保存受控组件需要使用的属性和事件处理函数 const newProps = { controlledProps: { value: this.state.value, onChange: this.handleValueChange } } return <wrappedComponent {...this.props} {...newProps}/> } } }
这个例子把受控组件 value 属性用到的状态和处理 value 变化的回调函数都提升到高阶组件中,当我们再使用受控组件时,就可以这样使用:
import React, { Component } from 'react' function withControlledState(wrappedComponent) { return class extends Component{ constructor(props) { super(props); this.state = { value: '' } this.handleValueChange = this.handleValueChange.bind(this); } handleValueChange(event) { this.this.setState({ value: event.EventTarget.value }) } render() { // newProps保存受控组件需要使用的属性和事件处理函数 const newProps = { controlledProps: { value: this.state.value, onChange: this.handleValueChange } } return <wrappedComponent {...this.props} {...newProps}/> } } } class SimpleControlledComponent extends React.Component { render() { // 此时的 SimpleControlledComponent 为无状态组件,状态由高阶组件维护 return <input name="simple" {...this.props.controlledProps}/> } } const ComponentWithControlledState = withControlledState(SimpleControlledComponent);
三、参数传递
高阶组件的参数并非只能是一个组件,它还可以接收其他参数。例如一中是从 LocalStorage 中获取 key 为 data的数据,当需要获取数据的 key不确定时,withPersistentData 这个高阶组件就不满足需要了。我们可以让它接收一个额外参数来决定从 LocalStorage 中获取哪个数据:
import React, { Component } from 'react' function withPersistentData(WrappedComponent, key) { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({ data }); } render() { // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件 return <WrappedComponent data={this.state.data} {...this.props} /> } } } class MyComponent extends Component { render() { return <div>{this.props.data}</div> } } // 获取 key='data' 的数据 const MyComponent1WithPersistentData = withPersistentData(MyComponent, 'data'); // 获取 key='name' 的数据 const MyComponent2WithPersistentData = withPersistentData(MyComponent, 'name');
新版本的 withPersistentData 满足获取不同 key 值的需求,但实际情况中,我们很少使用这种方式传递参数,而是采用更加灵活、更具能用性的函数形式:
HOC(...params)(WrappedComponent)
HOC(...params) 的返回值是一个高阶组件,高阶组件需要的参数是先传递 HOC 函数的。用这种形式改写 withPersistentData 如下(注意:这种形式的高阶组件使用箭头函数定义更为简洁):
import React, { Component } from 'react' const withPersistentData = (key) => (WrappedComponent) => { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({ data }); } render() { // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件 return <WrappedComponent data={this.state.data} {...this.props} /> } } } class MyComponent extends Component { render() { return <div>{this.props.data}</div> } } // 获取 key='data' 的数据 const MyComponent1WithPersistentData = withPersistentData('data')(MyComponent); // 获取 key='name' 的数据 const MyComponent2WithPersistentData = withPersistentData('name')(MyComponent);
四 、继承方式实现高阶组件
前面介绍的高阶组件的实现方式都是由高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,我们称这种方式为属性代理。除了属性代理外,还可以通过继承方式实现高阶组件:通过 继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。例如,当用户处于登录状态时,允许组件渲染,否则渲染一个空组件。代码如下:
function withAuth(WrappedComponent) { return class extends WrappedComponent { render() { if (this.props.loggedIn) { return super.render(); } else { return null; } } } }
根据 WrappedComponent的 this.props.loggedIn 判读用户是否已经登录,如果登录,就通过 super.render()调用 WrappedComponent 的 render 方法正常渲染组件,否则返回一个 null, 继承方式实现高阶组件对被包装组件具有侵入性,当组合多个高阶使用时,很容易因为子类组件忘记通过 super调用父类组件方法而导致逻辑丢失。因此,在使用高阶组件时,应尽量通过代理方式实现高阶组件。
以上主要参考 《React 进阶之路》这本书