React高阶组件

框架

浏览数:153

2019-10-2

高阶组件(HOC)是react为组件复用提供的一套更为先进的技术。HOC并非react所提供的api。他是react组件化思想的自然呈现。

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


const EnhancedComponent = higherOrderComponent(WrappedComponent);

然而一个componentprops转化为UI,高阶组件则是将一个compnent转化为另一个component

HOC在第三方库中很常见,比如Redux中的connect以及Relay中的createFragmentContainer

Use HOCs For Cross-Cutting Concerns

我们之前推荐使用
mixins来处理Cross-Cutting Concerns。现在我们已经意识到
mixins带来了更多的问题。
这里可以查阅到为什么我们移除了
mixins以及你如何改造已使用的
component

componentReact中最主要的组件复用单元。 However, you’ll find that some patterns aren’t a straightforward fit for traditional components.

比如,你现在持有一个订阅了外部数据源,用于渲染一项评论列表的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} />;
  }
}

CommentListBlogPost并非完全一致–他们调用DataSource中不同的函数,同时也渲染不同的输出内容。但其中大多数实现都是一致的:

  • 在加载完成后,添加一个数据改变的监听到DataSource
  • 在数据变化后,调用setState.
  • 卸载后,移除事件监听。

你可以想象到,在一个大型应用中,这些相同的数据订阅以及setState将会不断的重复。我们希望抽象出这部分逻辑,来使得我们可以在一处定义之后可以在多处组件中分享这套逻辑。这个就是高阶组件所擅长的地方。

我们可以通过一个函数来创建这个component,比如之前的CommentListBlogPost,都向DataSource进行订阅。The function will accept as one of its arguments a child component that receives the subscribed data as a prop. Let’s call the function withSubscription:

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

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

传入的第一个参数就是被包裹的component.第二个参数接受我们所需要的数据,这里是DataSource以及props

CommentListWithSubscription以及BlogPostWithSubscription需要进行渲染时,CommentListBlogPost会将从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} />;
    }
  };
}

注意:我们并没有修改输入的component,或者使用继承来复制他的逻辑。高阶组件通过一个容器组件来包裹原始组件。高阶组件是一个没有任何副作用的纯函数。

这就是关键点!这个被包裹的原始组件接受容器的所有props,与新的props,data以及任何它所需要的渲染的数据。高阶组件并不关心它的数据被怎样使用,而原始组件也不关心数据从哪里获取到。

由于withSubscription是一个普通函数,你可以添加你所需要的任意参数。比如,你希望data的读取的数据名可以配置,来进一步将被包裹的组件独立出来,或者你也可以接受一个参数来配置shouldComponentUpdate,又或者一个配置数据源的参数。以上所有操作都是可行的,因为高阶组件对于被包裹的原始组件拥有完整的控制权限。

withSubscription和被包裹的组件之间是完整的props-based。这使得他很容易将一个高阶组件替换到另一个,只要可以提供同样的props即可。这对于修改数据来源的库是非常有用的。

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

使用高阶组件来代替修改componentprototype 的方法(或是其他修改原型的办法)

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

这会产生一些问题.其一是传入的组件无法被复用。更为关键的是,如果你使用另一个类似方式来修改组件,它也会修改componentWillReceiveProps方法。那么前一次修改的逻辑就会被==覆盖==!这种方式对于函数组件也不会起作用,因为它们不存在生命周期函数。

修改原组件的方式来实现的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拥有与继承版本同样的功能,同时避免了潜在的冲突。它在class以及function component都能够良好的运作。而且由于这是一个纯函数,它能够与其他的HOC或者是它自己一起正常运作。

你可能已经听说过与HOC相似的模式–容器组件(container components).容器组件是分离高阶和低阶问题的策略中的一部分。容器管理着订阅,状态以及传递props到渲染UI的组件中。HOC使用容器作为实现逻辑的一部分。你可以认为HOC是参数化的容器组件定义。

约定:传递非相关Props到被包裹的组件

HOC给组件添加了特性。他们并非彻底地改变其逻辑。我们期望从HOC中返回的组件依然是拥有相似接口的组件。

HOC应该将与自身无关的props传递下去。大多数HOC都包含一个类似下面的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}
    />
  );
}

这个约定保证了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函数返回一个Component => Component模式的单参数HOC. 这样输出与输入类型一致的函数很容易组合在一起。

// 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被用作装饰器。这是一项实验性的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,对于使用高阶组件,我们有以下不太常用的提醒

不要使用HOC来替换render方法

React的差异算法使用组件标识来决定需要更新当前的子树或是丢弃现有的重新创建一个全新的。如果这组件通过render返回的结果与之前的完全一致(===),React会通过差异算法来递归的更新其子树。如果他们并不相同,之前的子树则会完整的卸载掉。

通常情况下,您不需要考虑这个方面。但这对于HOC非常重要,因为这意味着你无法应用HOCrender方法到被包裹的组件中。

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时,您也可以在组件的生命周期函数或者构造函数中完成。

必须复制组件的静态方法

有时候在组件中定义一个静态函数是非常有用的。比如,Relay对外暴露了getFragment的静态方法来促进GraphQL片段的组成。

当你应用了一个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;
}

当然,这需要您清楚的知道哪些方法需要被拷贝。您也可以使用hoist-non-react-statics来自动的拷贝所有的non-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并不会被传递

尽管习惯上高阶组件会将所有的props传递给被包裹的组件,但这个对引用并不起作用。这是因为ref并非是一个真实的prop–比如key,它会由React特别处理。如果你添加了一个ref到一个HOC返回的组件上,这个ref实际持有的是最外层的容器组件,而非被包裹的组件。

这个的解决方法是使用React.forwardRefAPI(在React 16.3 中引入).关于React.forwardRef

作者:不会爬树的猴