在函数式组件中使用HOC

February 24, 2021 ... ☕️ 4 min read

在目前函数替换class组件的的React开发趋势里,写HOC似乎变得有些复杂。究其原因,大概是由于函数式组件和高阶组件一样都是函数,而“把class传给一个函数,返回一个class”,和把函数传给一个函数,返回另一个函数,再混合“组件”的概念,理解起来难度高了一些。

高阶函数(Higher Order Function)

“把函数传给一个函数,返回另一个函数”,就是高阶函数。

高阶函数是一个常见的函数,它接受其他函数作为参数,然后返回一个函数。听起来很绕,但是这是个很常见的模式,比如有一个ajax函数,可以传一个callback作为处理函数,然后调用的地方直接取到这个值:

const dataHandler = (data) => {
  return `the return val is ${data}`;
};

const withData = (args, handler) => {
  return someAjaxFunc(args).then(
    (data) => {
      return handler(data);
    };
  );
};

这个withData其实做了什么呢?以函数的角度来看,它为dataHandlerk函数提供了必要的值,然后最终的逻辑还是交给dataHandler做。

这样一来,dataHandler就脱离了具体的数据依赖,例如可以通过withData,把不同请求来源,但处理流程相同的数据,统一使用一个方法来处理,使代码复用程度更高。

到这里,是不是有点React高阶组件的意思了?

高阶组件(Higher Order Component)

高阶组件实际是一个函数(不管对于class组件还是function组件),它接受一个组件,并返回一个新的组件。如Redux的connect,就用了高阶组件的思路。通常形式:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高级组件同样是为了提高代码的复用程度而引入的。

以React官方文档为例:有两个组件(CommentList和BlogPost),都需要获取外部数据,然后以组件形式,渲染不同内容。

class CommentList extends React.Component {
  constructor(props) {    super(props);    this.state = {      comments: []    };  }  componentDidMount() {    someAjaxCall(this.handleChange);  }  handleChange(data) {    // Update component state whenever the data source changes    this.setState({      comments: data    });  }
  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.state = {      blogPost: []    };  }  componentDidMount() {    someAjaxCall(this.handleChange);  }  handleChange(data) {    this.setState({      blogPost: data    });  }
  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

高亮的两部分处理逻辑基本是一样的:添加/移除数据监听器、重新获取数据。所以很自然的想到把这个逻辑抽离出来,作为一个高阶组件:

// This function takes a component...
function withSubscription(WrappedComponent, dataUrl) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: []
      };
    }

    componentDidMount() {
      someAjaxCall(dataUrl, this.handleChange);
    }

    handleChange(data) {
      this.setState({
        data: data
      });
    }

    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

const CommentListWithSubscription = withSubscription(
  CommentList,
  'https://commentListUrl'
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  'https://blogPostUrl'
);

其中CommentList和BlogPost负责输出不同的内容。

HOC组件只负责数据获取,不管最终如何使用。并且,可以通过props,传递任意的参数来扩充组件的功能。

那么,如果使用函数式组件,如何写一个HOC呢?

函数式组件的HOC

考虑一下API的React.memo函数,这就是一个HOC。它的使用方法是:

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

看,接受一个组件,然后返回一个新的组件。

如果自己写出来,就是下面这样:

// or
function withHooks(Element) {
  return function WithHooksHOC() {
    const [data, setData] = useState([]);
    useEffect(() => {
      someAjaxCall(url).then((result) => {
        setData(result);
      });
    }, []);
    return <Element data={data} {...props}/>;
  }
}
const CommentListWithSubscription = withHooks(CommentList, {url: 'https://commentListUrl'});
const BlogPostWithSubscription = withHooks(BlogPost, {url: 'https://blogPostUrl'});

或者

function withHooks(url) {
  return function WithHooksHOC(Element) {
    const [data, setData] = useState([]);
    useEffect(() => {
      someAjaxCall(url).then((result) => {
        setData(result);
      });
    }, []);
    return <Element data={data} {...props}/>;
  }
}
const CommentListWithSubscription = withHooks('https://commentListUrl')(CommentList);
const BlogPostWithSubscription = withHooks('https://blogPostUrl')(BlogPost);

二者不同之处只在于函数的柯里化:将 f(a,b,c) 转换为可以被以 f(a)(b)(c) 的形式进行调用。 如果同一个url用不同的组件处理,那么就可以进一步缩减代码:

const withDataSource = withHooks('https://commentListUrl');
const CommentListWithSubscription = withDataSource(CommentList);
const AnotherCommentListSubscription = withDataSource(AnotherCommentList);

除此之外,还可以使用render Props达到类似的效果。

参考文档

  1. Comparison: HOCs vs Render Props vs Hooks
  2. HOF and HOC (in React)
  3. Higher Order Functions & Currying

#React

SideEffect is a blog for front-end web development.
Code by Axiu / rss