高阶组件(HOC)
React官方定义高阶组件的概念是:
A higher-order component is a function that takes a component and returns a new component.
通常情况下,实现高阶组件的方式有以下两种:
属性代理(Props Proxy)
反向继承(Inheritance Inversion)
属性代理
实质上是通过包裹原来的组件来操作props,举个简单的例子:
import React, { Component } from 'React';
//高阶组件定义
const HOC = (WrappedComponent) =>
class WrapperComponent extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
//普通的组件
class WrappedComponent extends Component{
render(){
//....
}
}
//高阶组件使用
export default HOC(WrappedComponent)
我们可以看见函数HOC返回了新的组件(WrapperComponent),这个组件原封不动的返回作为参数的组件(也就是被包裹的组件:WrappedComponent),并将传给它的参数(props)全部传递给被包裹的组件(WrappedComponent)。这么看起来好像并没有什么作用,其实属性代理的作用还是非常强大的。
操作props
我们看到之前要传递给被包裹组件WrappedComponent的属性首先传递给了高阶组件返回的组件(WrapperComponent),这样我们就获得了props的控制权(这也就是为什么这种方法叫做属性代理)。我们可以按照需要对传入的props进行增加、删除、修改(当然修改带来的风险需要你自己来控制),举个例子:
const HOC = (WrappedComponent) =>
class WrapperComponent extends Component {
render() {
const newProps = {
name: 'HOC'
}
return <WrappedComponent
{...this.props}
{...newProps}
/>;
}
}
在上面的例子中,我们为被包裹组件(WrappedComponent)新增加了固定的name属性,因此WrappedComponent组件中就会多一个name的属性。
抽象state
属性代理的情况下,我们可以将被包裹组件(WrappedComponent)中的状态提到包裹组件中,一个常见的例子就是实现不受控组件到受控的组件的转变
class WrappedComponent extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
const HOC = (WrappedComponent) =>
class extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
this.onNameChange = this.onNameChange.bind(this);
}
onNameChange(event) {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange,
},
}
return <WrappedComponent {...this.props} {...newProps} />;
}
}
上面的例子中通过高阶组件,我们将不受控组件(WrappedComponent)成功的转变为受控组件.
用其他元素包裹组件
render(){
<div>
<WrappedComponent {...this.props} />
</div>
}
这种方式将被包裹组件包裹起来,来实现布局或者是样式的目的。
在属性代理这种方式实现的高阶组件,以上述为例,组件的渲染顺序是: 先WrappedComponent再WrapperComponent(执行ComponentDidMount的时间)。而卸载的顺序是先WrapperComponent再WrappedComponent(执行ComponentWillUnmount的时间)。
高阶组件的用法,其实就是封装个函数将传入的组件添加上数据,直接导出即可,我们常用的react-redux 中的 connect(Children) 一个道理,封装完将数据导入到组件当中,组件相应的具有数据,以及具有了dispatch方法,就是这么个封装。
话不多说直接上个小栗子:
class Parents extends Component {
constructor(props) {
super(props);
this.state = {
parentsSourse: '我是父组件数据'
}
}
render() {
<>
<Children />
这是父组件,相当于我们的外层组件
</>
}
}
class Children extends Component {
render() {
<>
这是子组件,我们展示组件
</>
}
}
我们假如我们想让父组件包含的组件都具有一个属性值,这个值是 newType: true, 此时我们可以直接向下级 Childlren 传递,那么我们也可以封装下父组件导出个高阶组件,那么这个方法可以这么写:
const Hoc_component = (HocCompoent) => {
return class NewComponent extends React.Component{
constructor(props){
super(props);
this.state={}
}
render() {
const props = { newType: true }
return <HocCompoent {...this.props} {...props}/>
}
}
}
// 此时所有的组件只要使用
Hoc_component(Children); // 此时的子组件就具有了这个方法包装的 newType属性,我们可以去打印看下。
下面的例子,我们把两个组件相似的生命周期方法提取出来,通过包装,能够节省非常多的重复代码。
// 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>
);
}
}
// BlogPost
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} />;
}
}
他们虽然是两个不同的组件,对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应该是一个没有副作用的方法。