[译]React高级指引7:高阶组件

原文连接:reactjs.org/docs/higher…html

引言

在React中高阶组件(HOC)是用于复用组件逻辑的一种高阶技巧。高阶组件自身并非React API的一部分。它是基于React组合特性而设计的一种模式。react

具体来讲,高阶组件就是一个接收组件做为参数并返回一个新组件的函数git

const EnhancedComponent = higherOrderComponent(WrappedComponent);
复制代码

组件将props转化成UI,而高阶组件则将组件转化成另外一个组件。github

高阶组件在第三方库中是十分常见的,好比Redux的connect,和Relay的createFragmentContainer算法

在本节中咱们将讲述为何高阶组件是有用的,如何来构建咱们本身的高阶组件。redux

使用高阶组件解决横切关注点问题

注意: 咱们以前推荐使用mixins来解决横切关注点问题。可是如今咱们已经了解到使用mixins会带来更多的问题。阅读更多了解为何咱们要抛弃mixins以及如何迁移已经编写好的组件。api

组件是React代码复用的基本单位。可是在实践过程当中中你会发现传统的组件没法直接适应某些模式。数组

好比,你如今有一个CommentList组件,它接收一个外部数据源来渲染评论:bash

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource"是某些全局数据源
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 注册change事件监听器
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除监听器
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源改变时更新state
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}
复制代码

以后,你编写了一个订阅单个博客帖子的组件,这个组件也是用了与上面相似的模式:app

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 <TextBlock text={this.state.blogPost} />;
  }
}
复制代码

CommentListBlogPost是不一样的——它们在DataSource上调用了不一样的方法而且渲染结果不一样。可是它们大部分的实现细节是相同的:

  • 在组件挂载完成后,为DataSource添加change监听器;
  • 在监听器内部,在数据源更改时调用setState;
  • 在组件卸载时移除监听器。

你能够想象,在一个大型应用中,这种订阅DataSource和调用setState的行为是一直存在的。咱们想要一个抽象方法,可以只在一个地方编写逻辑,而后把这段逻辑共享给须要的组件。这就是高阶组件擅长的地方。

咱们如今来建立一个函数,这个函数可以建立CommentListBlogPost,订阅DataSource。这个函数将会接收一个子组件做为它的参数之一,这个子组件将会接收订阅数据做为props。如今让咱们称这个函数为withSubscription

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
复制代码

第一个参数是被包裹的组件。第二个参数根据咱们给定的DataSource和prop返回咱们须要的数据。

CommentListWithSubscriptionBlogPostWithSubscription被渲染时CommentListBlogPost将会接收从当前DataSource中计算获得的数据做为data prop:

// 这个函数接收一个组件做为参数...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另外一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... 负责订阅的相关操做...
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      //... 用最新的数据渲染包裹的组件
      //注意咱们会传递其余数据
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
复制代码

注意高阶组件不会修改输入的组件,也不会用继承来复制它的行为。相反的,高阶组件将原始组件包裹在容器组件中。一个高阶组件应该是纯函数,没有任何反作用。

被包裹的组件从容器组件中获取了全部须要的props,同时也接收一个用于渲染的prop data。高阶组件不关心data是怎么被使用的,而被包裹的组件无论数据从哪来的。

这是由于withSubscription是一个正常的函数,你能够任意添加你想要的参数。好比你想要data prop的名字是可配置的。以进一步将高阶组件和被包裹的组件分离。或者你能够接受一个配置shouldComponentUpdate的参数,或者可以配置数据源的参数。因为高阶组件能够控制如何定义组件,因此这些都是可行的。

就像组件同样,withSubscription与被包裹组件的联系是彻底基于props的。这种关系使得更换高阶组件十分简单,只要可以提供一样的props给被包裹组件就能够了。好比这在你更换数据获取的第三方库时很是有用。

不要修改原始组件,使用组合

不要试图在高阶组件中修改组件的原型(prototype)或用其余任何方式修改它。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  //返回原始数组,暗示它已经被修改
  return InputComponent;
}

// EnhancedComponent将会在接收到prop时在控制台打印结果
const EnhancedComponent = logProps(InputComponent);
复制代码

这里有几个问题。一是输入组件没法像高阶组件加强以前使用了。更重要的是,若是你将EnhancedComponent包裹在另外一个能够修改EnhancedComponent的高阶组件中,那么第一个高阶组件的功能将被覆盖!同时这个高阶组件没法应用于没有生命周期的函数组件。

修改输入组件的高阶组件是一种糟糕的抽象方式——调用者必须知道它们是如何实现的以免与其余高阶组件发生冲突。

相比于修改,高阶组件应该使用组合,将输入组件包裹在一个容器组件中:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // 将输入组件包裹在容器组件中,而不是试图修改它
      return <WrappedComponent {...this.props} />;
    }
  }
}
复制代码

上面的高阶组件的功能和修改输入组件的高阶组件功能相同,可是避免了潜在的冲突问题。它可以很好地运用于class组件和函数组件。并且因为它是一个纯函数,它能够和其余高阶组件组合使用,甚至和它自身组合使用。

也许你已经发现了高阶组件和容器组件之间的相同之处。容器组件是分离高层关注和底层关注的策略之一。容器组件使用订阅和state管理事务,而且传递props给那些须要数据的组件。高阶组件使用容器组件做为实现自身的一部分。能够将高阶组件看成是参数化的容器组件。

约定:将不想管的props传递给被包裹的组件

高阶组件给组件添加了一些特性。它们自身不能大幅度修改约定。一般咱们但愿从高阶组件返回的组件与输入组件有类似的交互界面。

高阶组件应该透传与自身无关的props。大部分高阶组件包含了相似于下面的render方法:

render() {
  // 过滤出与高阶组件有关的额外props而且不透传它们。
  const { extraProp, ...passThroughProps } = this.props;

  // 将props注入被包裹组件。这些props一般是
  // state值或者实例函数
  const injectedProp = someStateOrInstanceMethod;

  // 将props传递给被包裹组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
复制代码

这个约定确保了高阶组件是灵活可复用的。

约定:最大化可组合性

并非全部的高阶组件都看起来是同样的。有时候高阶组件只接受一个参数:被包裹组件:

const NavbarWithRouter = withRouter(Navbar);
复制代码

一般高阶组件都会接收额外的参数。在下面的关于Relay的例子中,额外的参数config对象被用来声明组件的数据依赖:

const CommentWithRelay = Relay.createContainer(Comment, config);
复制代码

最多见高阶组件签名以下:

// React Redux的 `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
复制代码

这是什么鬼玩意??可是你把它分开,就能够更清晰地了解到它的机制。

// connect是一个函数,它返回了另外一个函数
const enhance = connect(commentListSelector, commentListActions);
// 返回的函数是一个高阶组件,它返回了一个
//与Redux store相关联的组件
const ConnectedComment = enhance(CommentList);
复制代码

换句话说,connect是一个高阶函数,它返回了一个高阶组件!

这种形式可能看起来让人困惑或没必要要,可是它有一个很是有用的属性。就像connect函数返回的单一参数高阶组件同样,它有一个签名Component => Component。输入类型与输出类型相同的函数是很是容易组合的。

// 不要这样...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... 你能够编写组合工具函数
// compose(f, g, h) 与 (...args) => f(g(h(...args)))相同
const enhance = compose(
  //它们都是单一参数高阶组件
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
复制代码

(一样的属性也容许connect和其余高阶组件承担装饰者的角色,装饰者是JavaScript一项实验性的提案。)

许多第三方库都提供了compose工具函数,好比lodash(lodash.flowRight),Redux,Ramda

约定:包裹显示名称一遍轻松调试

由高阶组件建立的容器组件会在React Developer Tools中像其余组件同样显示。为了能更好地调试,选择一个展现名称来显示它是高阶组件建立的组件。

最经常使用的方法是包裹被包裹组件的展现名称。因此若是你的高阶组件的名字是withSubscription,被包裹组件的展现名称是CommentList,那么就使用WithSubscription(CommentList)做为名称:

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
复制代码

注意事项

高阶组件有一些注意事项,对于刚接触React的人来讲可能不容易法相。

不要在render方法中使用高阶组件

React的diff算法使用组件的身份标志来决定是否更新子组件树仍是丢弃并从新挂载新的子组件树。若是render方法返回的组件与上一次渲染的组件一致(===),React将会根据diff算法在子组件树和新的子组件树进行递归更新。若是它们不是相同的,那么子组件树将会被彻底卸载。

正常来讲,你不须要考虑这个问题。可是这对高阶组件来讲很重要,由于者意味着你不能在组件的render方法中使用高阶组件来返回组件:

render() {
  //每一次更新时都会建立一个新的EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  //这样作会致使整个子组件树在每次渲染时都被卸载而后从新挂载!
  return <EnhancedComponent />;
}
复制代码

这不只仅是性能问题——从新挂载组件将会致使它的state状态和全部的子元素的丢失。

相反,若是在组件以外调用高阶组件,那么组件只会建立一次。在这以后,组件的身份标志将会在整个渲染过程当中保持一致。这才是咱们想要的。

尽管不多遇到,但有时候你仍是会须要动态地使用高阶组件,你能够在组件的生命周期方法或者构造函数中使用高阶组件。

务必复制静态方法

有时候在React组件中定义一个静态方法是十分有用的。好比,Relay容器暴露了一个getFragment静态方法来促进对GraphQL片断的组合。

当你将高阶组件应用于组件时,原始组件将被包裹在容器组件中。但这意味着新的组件将不持有任何原始组件的静态方法。

// 定义一个静态方法
WrappedComponent.staticMethod = function() {/*...*/}
// 如今调用一个高阶组件
const EnhancedComponent = enhance(WrappedComponent);

// 新的加强组件是没有静态方法的
typeof EnhancedComponent.staticMethod === 'undefined' // true
复制代码

为了解决这个问题,你能够在返回这个容器组件以前将这些静态方法复制给它:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  //必需要知道须要复制哪些方法
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
复制代码

然而上面的方法要求你必需要知道须要复制什么方法。你可使用hoist-non-react-statics来自动复制全部非React静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
复制代码

另外一种解决方法是再另外导出这个静态方法:

// 不要这么作...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独地将方法导出...
export { someFunction };

// ...在消费模块中,将这二者都引入
import MyComponent, { someFunction } from './MyComponent.js';
复制代码

Refs不会被透传

尽管高阶组件的规则是将全部的props都透传给被包裹组件,但对refs例外。这是由于refs不是真正的prop,就像key同样,它被React特殊对待。若是你为一个高阶组件产生的组件添加了ref,那么这个ref引用的是最外层的容器组件而不是被包裹的组件。

解决方案是使用React.forwardRef API(在React16.3中引进),在Refs转发中了解更多。

相关文章
相关标签/搜索