写点什么

你是如何使用 React 高阶组件的?

作者:beifeng1996
  • 2022-12-16
    浙江
  • 本文字数:3976 字

    阅读完需:约 13 分钟

High Order Component(包装组件,后面简称 HOC),是 React 开发中提高组件复用性的高级技巧。HOC 并不是 React 的 API,他是根据 React 的特性形成的一种开发模式。


HOC 具体上就是一个接受组件作为参数并返回一个新的组件的方法


const EnhancedComponent = higherOrderComponent(WrappedComponent)
复制代码


在 React 的第三方生态中,有非常多的使用,比如 Redux 的connect方法或者 React-Router 的withrouter方法。

举个例子

我们有两个组件:


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


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


他们虽然是两个不同的组件,对 DataSource 的需求也不同,但是他们有很多的内容是相似的:


  • 在组件渲染之后监听 DataSource

  • 在监听器里面调用 setState

  • 在 unmout 的时候删除监听器


在大型的工程开发里面,这种相似的代码会经常出现,那么如果有办法把这些相似代码提取并复用,对工程的可维护性和开发效率可以带来明显的提升。


使用 HOC 我们可以提供一个方法,并接受不了组件和一些组件间的区别配置作为参数,然后返回一个包装过的组件作为结果。


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


参考 前端进阶面试题详细解答


然后我们就可以通过简单的调用该方法来包装组件:


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


注意:在 HOC 中我们并没有修改输入的组件,也没有通过继承来扩展组件。HOC 是通过组合的方式来达到扩展组件的目的,一个 HOC 应该是一个没有副作用的方法。


在这个例子中我们把两个组件相似的生命周期方法提取出来,并提供 selectData 作为参数让输入组件可以选择自己想要的数据。因为 withSubscription 是个纯粹的方法,所以以后如果有相似的组件,都可以通过该方法进行包装,能够节省非常多的重复代码。

不要修改原始组件,使用组合进行功能扩展

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 receivedconst EnhancedComponent = logProps(InputComponent);
复制代码


通过以上方式我们也可以达到扩展组件的效果,但是会存在一些问题


  • 如果 InputComponent 本身也有componentWillReceiveProps生命周期方法,那么就会被覆盖

  • functional component 不适用,因为他根本不存在生命周期方法


修改原始组件的方式缺乏抽象化,使用者必须知道这个方法是如何实现的来避免上面提到的问题。


如果通过组合的方式来做,我们就可以避免这些问题


function logProps(InputComponent) {  return class extends React.Component{    componentWillReceiveProps(nextProps) {        console.log('Current props: ', this.props);        console.log('Next props: ', nextProps);    }    render() {        <InputComponent {...this.props} />    }  }}
// EnhancedComponent will log whenever props are receivedconst EnhancedComponent = logProps(InputComponent);
复制代码

惯例:无关的 props 传入到原始组件

HOC 组件会在原始组件的基础上增加一些扩展功能使用的 props,那么这些 props 就不应该传入到原始组件(当然有例外,比如 HOC 组件需要使用原始组件指定的 props),一般来说我们会这样处理 props:


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


extraProp是 HOC 组件中要用的 props,不用的剩下的 props 我们都认为是原始组件需要使用的 props,如果是两者通用的 props 你可以单独传递。

惯例:包装组件的显示名称来方便调试

function withSubscription(WrappedComponent) {  class WithSubscription extends React.Component {/* ... */}  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;  return WithSubscription;}
function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component';}
复制代码


简单来说就是通过手动指定displayName来让 HOC 组件能够更方便得被 react devtool 观察到

惯例:不要在 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 />;}
复制代码


一来每次调用enhance返回的都是一个新的 class,react 的 diffing 算法是根据组件的特征来判断是否需要重新渲染的,如果两次 render 的时候组件之间不是(===)完全相等的,那么会直接重新渲染,而部署根据 props 传入之后再进行 diff,对性能损耗非常大。并且重新渲染会让之前的组件的 state 和 children 全部丢失。


二来 React 的组件是通过 props 来改变其显示的,完全没有必要每次渲染动态产生一个组件,理论上需要在渲染时自定义的参数,都可以通过事先指定好 props 来实现可配置。

静态方法必须被拷贝

有时候会在组件的 class 上面外挂一下帮助方法,如果按照上面的方法进行包装,那么包装之后的 class 就没有来这些静态方法,这时候为了保持组件使用的一致性,一般我们会把这些静态方法拷贝到包装后的组件上。


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


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

ref

ref 作为 React 中的特殊属性--类似于 key,并不属于 props,也就是说我们使用传递 props 的方式并不会把 ref 传递进去,那么这时候如果我们在 HOC 组件上放一个 ref,拿到的是包装之后的组件而不是原始组件,这可能就会导致一些问题。


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
你是如何使用React高阶组件的?_React_beifeng1996_InfoQ写作社区