在函数式组件中使用HOC
2021年2月24日 • ... • ☕️ 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达到类似的效果。