React文档(二十四)高阶组件

高阶组件(HOC)是React里的高级技术为了应对重用组件的逻辑。HOCs本质上不是React API的一部分。它是从React的组合性质中显露出来的模式。html

具体来讲,一个高阶组件就是一个获取一个组件并返回一个组件的函数react

const EnhancedComponent = higherOrderComponent(WrappedComponent);

然而一个组件将props转变为UI,一个高阶组件将一个组件转变为另一个组件。git

HOCs在第三方React库里也是有的,就像Redux里的connect和Relay里的createContainer。github

在这篇文档里,咱们将讨论为何高阶组件有用处,还有怎样来写你本身的高阶组件。算法

为横切关注点使用HOCs数组

注意:app

咱们之前建议使用mixins来处理横切关注点的问题。但如今意识到mixins会形成不少问题。读取更多信息关于为何咱们移除mixins以及怎样过渡已存在的组件。ide

在React里组件是主要的重用代码单元。然而,你会发现一些模式并不直接适合传统的组件。函数

举个例子,假设你有一个CommentList组件它订阅了一个外部数据源来渲染一组评论:工具

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

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}
而后,你写一个组件来订阅一个单个博客帖子,遵循一个相似的模式:
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} />;
  }
}

CommentList和BlogPost不彻底相同。它们在DateSource上调用不一样的方法,而后它们渲染出不一样的输出。可是它们大多数实现是同样的:

  • 在初始装载的时候,给DataSource添加一个监听改变的监听器
  • 在监听器内部,当数据源变化的时候调用setState
  • 销毁的时候,移除监听器

你能够想象在一个大型app里,订阅到DataSource而且调用setState这个一样的模式会一遍又一遍的重复发生。咱们所以就想将这重复的过程抽象化,让咱们在一个单独的地方定义这段逻辑而且在多个组件中均可以使用这段逻辑。这就是高阶组件所擅长的。

咱们能够写一个建立组件的函数,就像CommentList和BlogPost,它们订阅到DataSource。这个函数会接受一个参数做为子组件,这个子组件接收订阅的数据做为prop。让咱们调用这个函数eithSubscription:

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

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

withSubscription的第一个参数是被包裹起来的组件。第二个参数检索咱们喜欢的数据,给出一个DateSource和当前的props。

但CommentListWithSubscription和BlogPostWithSubscription被渲染了,CommenList和BlogPost将被传递一个data属性包含当前DataSource里检索出的数据。

// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class 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 <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

注意一个HOC不会修改传入的参数组件,也不会使用继承来复制它的行为。不如说,一个HOC经过将原始组件包裹到一个容器组件里来混合。一个HOC是一个没有反作用的纯函数。

就是这样!被包裹的组件接受全部容器组件的props,还有和一个新的属性,data一块儿渲染它的输出。HOC不会关心数据怎样或者为何使用,被包裹的组件也不会关心数据是从哪里来的。

由于withSubscription是一个普通的函数,你能够添加或多或少的参数根据状况。举个例子,你也许想要使data属性的名字是可配置的,这样就能够进一步从包裹的组件隔离HOC。或者你能够接受一个参数来配置shouldComponentUpdate,或者一个参数来配置数据源。这些均可以由于HOC拥有全部权利去定义组件。

相似于组件,withSubscription和被包裹的组件之间的不一样是彻底基于props的。这就能够很容易地去交换一个HOC和另外一个,只要他们提供给被包裹组件的props是同样的。举个例子,这样若是你改变提取数据的库就会颇有用。

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

在HOC里要打消修改组件原型的想法。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  }
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);

这里有几个问题。一个问题就是输入的组件不能经过加强后的组件来分离地重用。更重要的,若是你为EnhancedComponent运用其余HOC那样也还会改变componentWillReceiveProps,先前的HOC的功能会被重写!这个HOC也不能凭借函数式组件来工做,也没有生命周期方法。

改变HOC是一个脆弱的抽象,用户必须知道他们是怎样实现的为了不和其余HOC发生冲突。

不用去修改,而应该使用组合,经过将输入的组件包裹到一个容器组件里:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

这个HOC和修改后的HOC功能同样然而避免了潜在的冲突。它和类组件还有函数组件都工做地很好。由于它是纯函数,因此它能够由其余HOC组成,甚至用它本身也能够。

你也许注意到了HOC和容器组件这个模式的类似之处。容器组件是将高阶和低阶关注点的功能分离的策略的一部分。

容器管理相似订阅和state的东西,而且传递props给组件而后处理相似渲染UI的事。HOC将容器做为实现的一部分。你能够把HOC看作参数化的容器组件的定义。

约定:给被包裹的元素传递不相关的props

HOC给一个组件添加特性。它们不该该完全改变它的约定。那就是HOC返回的组件拥有一个和被包裹的组件相似的界面。

HOC应该传递与肯定的关注点不相关的props。多数HOC包含一个渲染方法看起来就像这样:

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

这个约定确保HOC尽量地灵活和可重用。

约定:最大化可组合性

不是全部的高阶组件看起来都同样。有时候它们只接收一个参数,被包裹的组件:

const NavbarWithRouter = withRouter(Navbar);

一般HOC会接收额外的参数。在Relay的例子里,一个config对象被用于指定组件的数据依赖:

const CommentWithRelay = Relay.createContainer(Comment, config);

HOC最多见的签名是这样的:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);

什么?!若是你将步骤分离,就能够很容易看出发生了什么。

// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is an HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);

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

这种形式也许看起来让人迷惑或者不是很重要,可是它有一个有用的属性。单参数的HOC例如connect函数返回的那一个拥有这样的鲜明特征Component => Component(组件 => 组件)。输出类型和输入类型相同的函数就很容易组合到一块儿。

// Instead of doing this...
const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent))

// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
  // These are both single-argument HOCs
  connect(commentSelector),
  withRouter
)
const EnhancedComponent = enhance(WrappedComponent)

(这个同样的属性一样容许connect和其余加强器HOC被做为装饰来使用,这是一个实验性的js提案)

compose这个实用函数是不少第三方库提供的,包括lodash(lodash.flowRight),Redux,Ramda。

约定:包裹显示名字为了使调试更加简单

就像其余组件同样,HOC建立的容器组件也会显示在React Developer Tools工具里。想让调试更加简单,选择一个显示名字来通信这是一个HOC的结果。

最广泛的技术是包裹被包裹函数的显示名字。因此若是你的高阶函数的名字是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函数里使用HOC

React的diffing算法(被称为一致)使用组件的一致来决定是否应该更新已存在的子树或者放弃更新或者从新建立一个新的。若是render返回的组件和上一次render的组件同样(===),React就会经过比较二者的不一样来递归地更新子树。若是它们不同,那么先前的子树就彻底被销毁。
一般,你不须要思考这些。可是这对HOC来讲很重要由于这意味着你不能在render方法里运用HOC。
render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}

这里的问题不仅只是性能--一个组件的重载会形成组件的state和它全部的子元素丢失。

在组件定义的外部来使用HOC使得结果组件只被建立一次。以后,它的身份在渲染过程当中会一直保持不变。总之,这就是你常常想要的结果。

在这些罕见的状况里你须要动态的运用HOC,你也能够在组建的生命周期函数里或者构造函数里使用。

静态方法必须被复制

有些时候在React组件里定义一个静态方法是颇有用的。举个例子,Relay容器暴露了一个静态方法getFragment为了促进GraphQL片断的组成。

当你为一个组件运用了HOC,虽然原始组件被一个容器组件所包裹。这意味着新的组件没有任何原始组件的静态方法。

// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply an 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;
}

然而,这要求你知道哪个方法须要被复制。你可使用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;
}

另外一个可能的解决方案是分离地输出静态方法。

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
相关文章
相关标签/搜索