[译]React高级话题之高阶组件

前言

本文为意译,翻译过程当中掺杂本人的理解,若有误导,请放弃继续阅读。javascript

原文地址:Higher-Order Componentshtml

正文

高阶组件(后文中均以HOCs来指代)是React生态里面的一种用来复用组件逻辑的高级技术。HOCs自己并非React API的一部分,而是一种从React的可组合性中产生的模式。java

具体来讲,HOCs其实就是一个函数。只不过这个函数跟编程语言中普通的函数不一样的是,它接受一个React组件做为输入,返回了一个新的React组件。react

const EnhancedComponent = higherOrderComponent(WrapperComponent)
复制代码

咱们从转化的角度能够这么说:“若是说,React组件是将props转化为UI,那么HOCs就是将一个旧的组件转化为一个新的组件(通常状况下,是做了加强)”。HOCs在第三方类库中很常见,好比:Redux的connect方法,Relay的createFragmentContainergit

在这个文档里面,我么将会讨论为何HOCs这么有用和咱们该怎样写本身的高阶组件。github

使用HOCs来完成关注点分离

注意:咱们以前一直在推荐使用mixins来完成关注点分离。可是后面咱们发现了mixins所带来的问题远大于它存在所带来的价值,咱们就放弃使用它了。查阅这里,看看咱们为何放弃了mixins,而且看看你能够如何升级你的现有组件。算法

在React中,组件是代码复用的基本单元。然而你会发现,一些模式并不能简单地适用于传统意义上的组件。编程

举个例子来讲,假设你有一个叫CommentList的组件。这个组件订阅了一个外部的数据源,最终将渲染出组件列表。redux

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() {
    // 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> ); } } 复制代码

然后,你又以相同的模式去写了一个用于订阅一篇博客文章的组件。以下:api

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是不彻底相同的。它们分别在DataSource上调用不一样的方法,渲染了不一样的UI。可是,除了这些点以外,它们大部分是相同的:

  • 在挂载以后,都往DataSource里面注册了一个change listener。
  • 在change listener里面,当数据源发生改变时都调用了setState。
  • 在卸载以前,都要移除change listener。

你能够想象,在一个大型的项目中,这种模式的代码(订阅一个DataSource,而后在数据发生变化的时候,调用setState来更新UI)会处处出现。咱们须要将这种逻辑抽取出来,定义在单独的地方,而后跨组件去共用它。而,这偏偏是HOCs要作的事情。

咱们能够写一个函数用于建立像CommentList和BlogPost那样订阅了DataSource的组件。这个函数将会接收子组件做为它的一个参数。而后这个子组件会接收订阅的数据做为它的prop。咱们姑且称这个函数为withSubscription。

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

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

第一个参数是被包裹的组件(wrapped component),第二个参数是一个函数,负责经过咱们传递进去的DataSource和props来获取并返回咱们须要的数据。

当CommentListWithSubscription和BlogPostWithSubscription被渲染以后,组件CommentList和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} />; } }; } 复制代码

注意,HOCs并无篡改咱们传递进入的组件,也没有继承它而后复制它的行为。HOCs只是单纯地将咱们传递进入的组件包裹在一个容器组件(container component)。一个高阶组件是纯函数,不能包含任何的反作用。

就这么多。wrapped component接受从container component传递下来的全部props,与此同时,还多了一个用于渲染最终UI的,新的prop:data。HOC它不关注你怎么使用数据,为何要这样使用。而wrapped component也不关心数据是从哪里来的。

由于withSubscription只是一个普通函数,你能够定义任意多的参数。举个例子,你想让data 属性变得更加的可配置,以便将HOC和wrapped component做进一步的解耦。又或者你能够增长一个参数来定义shouldComponentUpdate的实现。这些都是能够作到的,由于HOC只是一个纯函数而已,它对组件的定义拥有百分百的话语权。

正如React组件同样,高阶组件withSubscription跟wrapped component的惟一关联点只有props。这样的清晰的关注点分离,使得wrapped component与其余HOC的结合易如反掌。前提是,另一个HOC也提供相同的props给wrapped component。就拿上面的例子来讲,若是你切换data-fetching类库(DataSource),这将会是很简单的。

戒律

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

在HOC的内部,要抵制修改原始组件的prototype的这种诱惑(毕竟这种诱惑是触手可及的)。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(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,刚好这个HOC在原型链上也作了一样的修改。那么,你第一个HOC的功能就被覆盖掉了。
  • 上述例子中的写法不能应用于function component。由于function component没有生命周期函数。
  • 形成抽象封装上的漏洞。一旦你这么作了,那么使用者为了不冲突,他必须知道你到底在上一个HOC对wrapped component 作了什么样的修改,以避免他也做出一样的修改而致使冲突。

相对于修改,HOCs应该使用组合来实现。也就是说,把传递进来的组件包裹到container component当中。

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,它很好地避免了一些潜在的冲突。同时,它也能很好地跟function component和class component组合使用。最后,它也能很方便地跟其余HOC组合使用,或者甚至跟它本身。

container component是对高层级关注点与低层级关注点进行职责分离策略的一部分。在这个策略里面,container component负责管理数据订阅和保存state,而且将全部的数据衍生为props传递给它的子组件,而后子组件负责渲染UI。HOCs把container模式做为了它实现的一部分。你能够理解为HOC是参数化的container component定义。

2.不要在React组件的render方法中使用HOCs

React的diff算法(也称之为reconciliation)是根据component的惟一标识(component identity )来决定这个组件是否应该从已经存在的子组件树中更新仍是完全弃用它,挂载一个新的组件。若是一个component的render函数的返回值全等于(===)另一个组件render函数的返回值,那么React就认为他们是同一个组件,而后递归地更新这个组件的子组件树。不然的话,就彻底卸载以前的那个组件。

一般来讲,你不须要考虑这些东西。可是,在使用HOCs的时候,你须要作这样的考虑。由于你不能在一个组件的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和子组件树。 而咱们想要的偏偏相反。咱们想要的加强后的组件的标识在屡次render调用过程当中都是一致的。要想达成这种效果,咱们须要在组件定义的外部去调用HOC来仅仅建立一次这个加强组件。

在极少数的状况下,你可能想动态地使用HOC,你能够在组件的非render生命周期函数或者constructor里面这么作。

约定俗成

1. 将(HOC)非相关的props传递给Wrapped component

HOCs本质就是给组件增长新的特性。他们不该该去大幅度地修改它与wrapped component的契约之所在-props。咱们期待从HOC返回的新的组件与wrapped component拥有相同的接口(指的也是props)。

HOCs应该将它不关注的props原样地传递下去(给加强后的新组件)。大部分的HOCs都会包含一个render方法,这个render方法看起来是这样的:

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} /> ); } 复制代码

咱们这么作的目的是让HOCs能作到尽量的可扩展和可复用。

2. 可组合性最大化

并非全部的HOCs看起来都是同样的。有一些HOC仅仅接收一个参数-wrapped component。

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

通常来讲,HOCs会接收其他的一些参数。好比说Relay的createContainer方法,它的第二个参数就是一个配置型的参数,用于指明组件的数据依赖。

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

HOCs最多见的函数签名是这样的:

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

什么鬼?若是你把这行代码拆开来看,你就会知道这究竟是怎么回事。

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

换句话说,connect就是一个返回高阶组件的高阶函数。(注意,原文这里用的是higher-orderfunction 和 higher-order component)!

这种写法可能看起来是让人疑惑或者是多余的,实际上,它是有用的。从函数式编程的角度来说,那种参数类型和返回类型一致的单参数函数是很容易组合的。而connect之因此要这么实现,也是基于这种考虑。也就是说,相比这种签名的函数(arg1,component)=> component,component => component 类型的函数更容易跟同类型的函数组合使用。具体的示例以下:

// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(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
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
复制代码

像connect的这种类型写法的函数能够被用做ES7提案特性之一的装饰器(decorators)。

像compose这种工具函数不少第三方的类库都会提供,好比说lodash(loadash.flowRight),ReduxRamda

3. 给HOC追加displayName属性

那个被HOCs建立的container component在React Developer Tools中长得跟普通的组件是同样的。为了更方便调试,咱们要选择一个display name 给它。

最多见的作法是给container component的静态属性displayName直接赋值。假如你的高阶组件叫withSubscription,wrapped component叫CommentList,那么container component的displayName的值就是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';
}
复制代码

注意点

1. 记得要把Wrapped component的静态方法复制到加强后的组件中去

有时候,在React component上定义一个静态方法仍是挺有用的。好比说,Relay container就暴露了一个叫getFragment静态方法来方便与GraphQL fragments的组合。

当你把一个组件传递进HOC,也仅仅意味着你把它包裹在container component当中而已。由于加强后的组件并无“继承”原组件的全部静态方法。

// 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
复制代码

为了解决这个问题,你能够在HOC的内部先将原组件的静态方法一一复制了,再返回出去。

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';
复制代码

2. 记得将ref属性传递下去

虽说,将全部的prop都原样传递下去是实现HOC的一个惯例,可是这种传递对ref这个属性不起做用。那是由于,严格意义上说,ref并非一个真正的prop,key也不是。它俩都是专用于React的内部实现的。若是你在一个由HOC建立并返回的组件(也就是说加强后的组件)上增长了ref属性,那么这个ref属性指向的将会是HOC内部container component最外层那个组件的实例,而不是咱们期待的wrapped component。

针对这个问题的解决方案是使用React.forwardRef这个API(在React的16.3版本引入的)。关于React.forwardRef,你能够查阅更多

相关文章
相关标签/搜索