React高阶组件

最后更新时间:2019/05/15
以下内容来自:

  • React 官网文档
  • Robin Wieruch 博客
  • The Road to learn React Your journey to master plain yet pragmatic React.js
  • 自身理解:(

1 HOC 基础概念

1.1 定义

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature

They take any input - most of the time a component, but also optional arguments - and return a component as output. The returned component is an enhanced version of the input component and can be used in your JSX

const EnhancedComponent = higherOrderComponent(WrappedComponent);

1.2 理解

  1. 高阶组件只是 React 建议的一种机制、模式,并非一个特殊的 API。
  2. HOC 的目的在于通过将不同的 Component 中相同的逻辑提取出来,在一个 function 实现这些通用逻辑,之后接受 Component 输入,“注入”通用逻辑,实现对 component 的增强,减少代码冗余,提高组件的复用性。这种通用逻辑的注入可以是向 Component 注入新的 prop,可以是对 Component 的 prop 进行某种检查,进行条件渲染等。
  3. HOC 的返回可以是一个 class 组件,function 组件或者另外的 HOC
  4. 传入的 Component 作为return新的增强的组件的相对独立的一部分,因此 不要在高阶组件中直接修改传入组件(方法等)

2 实例

2.1 对 Component 注入新的 prop

2.1.1 思考与使用过程

现在存在两个组件,CommentListBlogPost,他们都从一个外部数据DataSource中获取数据进行展示。

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments(),
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      comments: DataSource.getComments(),
    });
  }

  render() {
    return (
      
{this.state.comments.map(comment => ( ))}
); } }
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id),
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id),
    });
  }

  render() {
    return ;
  }
}

两个组件的区别:

  1. DataSource中获取数据的方法不同,一个是getComments,一个是getBlogPost
  2. 展示数据的render函数不同。

相同点:

  1. 在组件挂载时 subscribe DataSource,当DataSource发生改变后,调用handleChange重新渲染;同时卸载时移除 listener
  2. 都从DataSource中获取数据

可以看到这两个组件存在相同的逻辑,即从DataSource中获取数据,进行渲染。当中存在冗余的代码,如果再写第三个组件,如IssueList,那么这个逻辑还要重复一个。
因此我们可以采用以下的高阶组件提取通用逻辑:

(1)定义高阶组件
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class EnhancedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props),
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props),
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return ;
    }
  };
}

此 HOC 接受WrappedComponent,以及selectData两个参数,前者是需要增强的组件,后者是用来从DataSource中获取数据的 function。
注意到 HOC 返回的是一个增强的、新的 class react 组件,具有以下几个特征:

  1. local state中保存了通过 HOC 参数selectData拿到的数据
  2. render函数返回的还是传入的WrappedComponent组件的实例,并且传入了一个新的data属性。
  3. 需要注意,{...this.props},保证了高阶组件实例生成时定传入的props都能够传入WrappedComponent组件。
(2)重新定义原组件

重新实现之前的CommentListBlogPost组件,此时在它们的render函数中直接使用this.props.data来进行渲染,不需要再与DataSource进行交互。

class CommentList extends React.Component {
  render() {
    const { data, ...res } = this.props;
    return (
      
{data.map(comment => ( ))}
); } }
class CommentList extends React.Component {
  render() {
    const { data, ...res } = this.props;
    return ;
  }
}
(3)定义增强组件
const CommentListWithSubscription = withSubscription(CommentList, DataSource =>
  DataSource.getComments(),
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id),
);

此时的CommentListWithSubscriptionBlogPostWithSubscription是高阶组件withSubscription返回的新增强的 class 组件。

(4) 使用新的增强组件
class App extends Component{
    ...
    render() {
        ...
        return (
            
) } }

注意其中的disableed属性会一层层的传递给Comment组件,传递过程如下:

  1. 首先是传入withSubscription返回的EnhancedComponent组件 render 函数中的props
  2. 通过 return 语句中{...this.props}被传递给组件
  3. 此时WrappedComponentCommentList,在它的 render 函数可通过{...res}传递给

2.1.2 总结

可以看到这种方式的 HOC 没有直接改变传入的 Component,而是传入新的prop,因此,在 Component 的render函数中可以使用新的prop进行渲染或其它操作。以上例子在React docs进一步了解。

2.2 条件渲染

现在存在一个ToDoList组件

function TodoList({ todos, isLoadingTodos }) {
  if (isLoadingTodos) {
    return (
      

Loading todos ...

); } if (!todos) { return null; } if (!todos.length) { return (

You have no Todos.

); } return (
{todos.map(todo => ( ))}
); }

可以看到有很多关于 todos 的条件渲染,我们可以尝试把这种条件渲染的逻辑提取出来,形成下面的情况:

const withLoadingIndicator = Component => ({ isLoadingTodos, ...others }) =>
  isLoadingTodos ? (
    

Loading todos ...

) : ( ); // (1) const withTodosNull = Component => props => !props.todos ? null : ; // (2) const withTodosEmpty = Component => props => !props.todos.length ? (

You have no Todos.

) : ( ); // (3)

重新定义ToDoList组件:

const TodoList = ({ todos }) => (
  
{todos.map(todo => ( ))}
);

之后利用这三个新的 HOC,定义新的增强组件:

const TodoListWithConditionalRendering = withLoadingIndicator(
  withTodosNull(withTodosEmpty(TodoList)),
);
// 可以写成以下的形式
// const TodoListOne = withTodosEmpty(TodoList);
// const TodoListTwo = withTodosNull(TodoListOne);
// const TodoListThree = withLoadingIndicator(TodoListTwo);

现在,生成增强组件的实例:

    ...
    
    ...

isLoadingTodos属性通过 (1)(2)(3)层层传递给TodoList组件。
更多的了解可以参看A gentle Introduction to React's Higher Order Components

2.3 第三方库 HOC 例子

让我们来看实际的例子:

// antd Form组件的使用 参见https://ant.design/components/form-cn/#Form.create(options)
import { Form } from 'antd';

class CustomizedForm extends React.Component {}

export default (CustomizedForm = Form.create({})(CustomizedForm));

代码中的Form.create()方法接受一个option参数,该参数的部分属性如下表:

参数 说明
name 设置表单域内字段id的前缀
onValuesChange 任一表单域的值发生改变时的回调

一个使用的例子如下:

const CustomizedForm = Form.create({
  name: 'global_state',
  onFieldsChange(props, changedFields) {
    props.onChange(changedFields);
  },
  mapPropsToFields(props) {
    return {
      username: Form.createFormField({
        ...props.username,
        value: props.username.value,
      }),
    };
  },
  onValuesChange(_, values) {
    console.log(values);
  },
})(CustomComponent);

Form.create()方法返回的还是一个 HOC,这个 HOC 单独接受一个组件输入,返回增强组件,即上面的Form.create({})(CustomizedForm)
此时在CustomizedForm组件中就可以使用被高阶组件注入的属性form。例如:

// CustomizedForm
render() {
    const {
      getFieldDecorator, getFieldsError, getFieldError, isFieldTouched,
    } = this.props.form;

    // Only show error after a field is touched.
    const userNameError = isFieldTouched('userName') && getFieldError('userName');
    return (
      
{getFieldDecorator('userName', { rules: [{ required: true, message: 'Please input your username!' }], })( } placeholder="Username" /> )}
); }

2.4 使用注意事项

  1. 定义的 HOC 可以采用with开头

  2. 不要在render中使用 HOC,而是在render之外就使用 HOC 定义好新的增强组件,在 render 函数中直接使用 HOC 返回的增强组件。原因有以下两点:

    • 效率:每次render执行时,都使用 HOC 生成新的增强组件,一方面效率较低,另一方面virtual DOMreal DOM比较时,新的增强组件与旧的组件不会认为是相同的。
    • 增强组件的状态丢失:每次render执行会卸载之前的增强组件,导致其中的state丢失
  3. 静态方法需要特别对待:如果在原始组件中定义了静态方法,之后使用 HOC 返回的增强组件是没有该静态方法的。如:

    // Define a static method
    WrappedComponent.staticMethod = function() {
      /*...*/
    };
    // Now apply a HOC
    const EnhancedComponent = enhance(WrappedComponent);
    
    // The enhanced component has no static method
    typeof EnhancedComponent.staticMethod === 'undefined'; // true
    

    要解决这个问题,需要拷贝该静态方法:

    function enhance(WrappedComponent) {
      class Enhance extends React.Component {
        /*...*/
      }
      // Must know exactly which method(s) to copy :(
      Enhance.staticMethod = WrappedComponent.staticMethod;
      return Enhance;
    }
    
  4. ref属性无法传递,原因在于ref不是和其它普通 prop 一起存在props中的,它会被 React 特殊处理,ref只会指向增强组件,而不是被包裹的原始 Component。解决这个问题在于使用React.forwardRef, Learn more about it in the forwarding refs section.

你可能感兴趣的:(React高阶组件)