React 高阶组件

在React中高阶组件(HOC)是一种为了重用组件逻辑的增强技术。就本身来说,高阶组件并不是React API的一部分。它是由React的组合特性中衍生出的一种模式。

具体地说,高阶组件是一个接受组件并返回新组件的函数

const EnhancedComponent = higherOrderComponent(WrappedComponent);

如同组件将属性转换为UI,而高阶组件将组件转换为另一个组件。

高阶组件在第三方React库中很常见,例如,Redux的connect以及Relay的createFragmentContainer

在本文中,我们将讨论为什么高阶组件是有用的,以及如何编写自己的组件。

使用高阶组件横切关注点

注意:我们之前推荐使用mixins来处理横切关注点。从那以后,我们意识到mixins带来的麻烦比它们的价值还多。阅读更多关于为什么我们已经远离mixins以及如何转换现有组件的信息。

组件是React中代码重用的主要单元。但是,您会发现有些模式并不适合传统组件。

例如,假设您有一个CommentList组件,该组件订阅外部数据源以呈现评论列表:

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>
    );
  }
}

稍后,您将编写订阅单个博客文章的组件,该组件遵循类似的模式:

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上调用不同的方法,并呈现不同的输出。但它们的大部分实现是相同的:

  • 在挂载时,将更改侦听器添加到DataSource
  • 在侦听器内部,每当数据源发生更改时就调用setState
  • 在卸载时,删除更改侦听器。

您可以想象,在大型应用程序中,订阅DataSource和调用setState的相同模式将反复出现。我们需要一个抽象,它允许我们在一个地方定义这个逻辑,并在多个组件之间共享它。这就是高阶组件擅长的地方。

我们可以编写一个函数来创建订阅DataSource的组件,如CommentListBlogPost。该函数将接收作为其参数之一的子组件,该组件将接收订阅的数据作为一个属性。我们把这个函数命名为withSubscription

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

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

第一个参数是包装组件。第二个参数接收我们感兴趣的数据,即给定DataSource和当前的属性。

CommentListWithSubscriptionBlogPostWithSubscription被渲染时,CommentListBlogPost将被传递一个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是一个没有任何副作用的纯函数。

这就是高阶组件!包装的组件接收容器的所有属性,以及一个新属性data,用于呈现其输出。HOC不关心如何或为什么使用数据,而封装的组件不关心数据来自何处。

因为withSubscription是一个普通的函数,您可以添加任意多的参数,也可以添加任意少的参数。例如,您可能希望将data属性的名称变为可配置的,以进一步将HOC与包装的组件隔离。或者您可以接受一个配置shouldComponentUpdate的参数,或者一个配置数据源的参数。这些都是可能的,因为HOC完全控制组件的定义方式。

与组件一样,withSubscription和包装组件之间的契约完全是基于属性的。只要它们为包装的组件提供相同的属性,就可以轻松地将一个高阶组件切换为另一个高阶组件。例如,如果更改数据获取库,这可能会很有用。

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

请抵制在HOC内部修改组件原型(或以其他方式使其发生变化)的诱惑。

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,它也会修改componentWillReceiveProps,那么第一个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与修改版本具有相同的功能,同时避免了潜在的冲突。它同样适用于类和函数组件。因为它是一个纯函数,它可以和其他高阶组件组合,甚至可以和它自己组合。

您可能已经注意到高阶组件与容器组件之间的相似性。容器组件是分离高层次和低层次关注之间的责任的策略的一部分。容器管理订阅和状态之类的事情,并将属性传递给处理呈现UI之类事情的组件。高阶组件使用容器作为其实现的一部分。可以将高阶组件看作参数化的容器组件定义。

约定:将不相关的属性传递给包装好的组件

高阶组件为组件添加特性。他们不应该大幅度修改约定。从HOC返回的组件应该与封装的组件具有类似的接口。HOC可能需要传递与其特定关注点无关的属性。大多数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尽可能灵活和可重用。

约定:最大化可组合性

不是所有的HOC看起来都一样。有时它们只接受一个参数,即包装后的组件:

const NavbarWithRouter = withRouter(Navbar);

通常,HOC接受附加的参数。在这个来自于Relay的例子中,使用了一个配置对象来指定组件的数据依赖关系:

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

HOC最常见的签名如下:

// 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是一个返回高阶组件的高阶函数!

这种形式可能看起来令人困惑或不必要,但它有一个有用的属性。connect函数返回的单参数HOC具有签名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和其他增强样式的hoc用作装饰器,这是一个实验性的JavaScript提案。)

许多第三方库都提供了compose函数,包括lodash(例如lodash. flowright)、Redux和Ramda。

约定:包装显示名称以便于调试

由hoc创建的容器组件与其他组件一样显示在React Developer工具中。要简化调试,请选择一个显示名称,该名称表示它是特定的结果。

最常用的技术是包装被包装组件的显示名称。因此,如果您的高阶组件名为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';
}

警告

高阶组件附带一些注意事项,如果您是新手,这些注意事项不会立即显现出来。

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

React的扩散算法(称为协调)使用组件标识来决定是更新现有的子树还是丢弃它并挂载一个新树。如果从render返回的组件与前一个render返回的组件相同(===),那么React递归地更新子树,将其与新的子树区分开来。如果它们不相等,则完全卸载前一个子树。

通常,你不需要考虑这个。但这对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 />;
}

这里的问题不仅仅是性能——重新挂载组件会导致组件及其所有子组件的状态丢失。

相反,应该在组件定义之外应用hoc,以便只创建一次结果组件。然后,在渲染期间它的身份将是一致的。不管怎样,这通常是你想要的。

在需要动态应用HOC的罕见情况下,也可以在组件的生命周期方法或其构造函数中进行。

必须复制静态方法

有时在React组件上定义静态方法很有用。例如,Relay 容器公开一个静态方法getFragment,以便于组合GraphQL片段。

但是,当您将高阶组件应用于组件时,原始组件将被容器组件包装。这意味着新组件没有任何原始组件的静态方法。

// 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;
}

但是,这需要您确切地知道需要复制哪些方法。您可以使用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';

无法传递引用

虽然高阶组件的约定是通过将所有的属性传递到包装的组件,但这对refs不起作用。这是因为和key一样,ref并不是一个真实的属性,它被React特殊处理。如果你为高阶组件的元素添加了引用,这个引用指向最外层的容器组件,而不是被包裹的组件。

原文链接:https://reactjs.org/docs/higher-order-components.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值