高阶组件
高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组建的函数。
const EnhanceComponent = higherOrderComponent(WrappedComponent)
组件是将props转化为ui,而高级组件是将组件转化为另一个组件。
HOC在React的第三方库中非常常见,例如Redux的connect
和Relay的CreateFragmentContainer。
使用HOC解决横切关注点问题
组件是React中代码复用的基本单元。但是你会发现某些模式并不适合传统组件。
例如,假如有一个CommentList
组件,它订阅外部数据源,用以渲染评论列表:
const DataSource ={
}
class CommentList extends React.Component {
constructor(props){
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
comments:DataSource.getComments()
}
}
componentDidMount(){
// 订阅更改
DataSource.addChangeListener(this.handleChange)
}
componentDidUnmount(){
// 清楚订阅
DataSource.removeChangeListener(this.handleChange)
}
handleChange(){
this.state = {
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} />;
}
}
CommentList
和BlogPost
不同的地方:他们在DataSource
上调用不同的方法,且渲染不同的结果。但他们的大部分实现都是一样的:
- 在挂载时,向
DataSource
添加一个更改的侦听器 - 在侦听器内部,当数据源变化时候,调用
setState
- 在卸载时,删除侦听器。
你可以想象,在一个大型应用程序中,这种订阅DataSource
和调用setState
的模式,将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。
对于订阅了DataSource
的组件,比如CommentList
和BlogPost
,我们可以编写一个创建组件函数。该函数接受一个子组件作为它的其中一个参数,该子组件将订阅数据座位props。让我们调用函数withSubscription
:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
第一个参数是被包装组件,第二个参数是通过DataSource
和当前的props返回我们需要的数据。
当渲染 CommentListWithSubscription 和 BlogPostWithSubscription 时, CommentList 和 BlogPost 将传递一个 data prop,其中包含从 DataSource 检索到的最新数据:
function withSubscription(WrappedComponent,selectData){
return class extends React.Component{
constructor(props){
super(props)
this.handleChange = this.handleChange.bind(this)
this.state = {
data:selectData(DataSource,props)
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
}
}
请注意,HOC不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC通过将组件包装在容器组件中来组成新组件。HOC是纯函数,没有副作用。
被包装组件接收来自容器组件的所有props,同时也接收一个新的用于render的data prop。
HOC不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。
因为withSubscription
是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,你可能希望使data
prop的名称可配置,以进一步将HOC与包装组件隔离开来。或者你可以接受一个配置shouldComponentUpdate
的参数,或者一个配置数据源的参数。因为HOC可以控制组件的定义方式,这一切都变得有可能。
与组件一样,withSubscription
和包装组件的契约完全基于之间传递的props。这种依赖方式使得替换HOC变得非常容易,只要它们为包装的组件提供相同的prop即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。
不要改变原始组件。使用组合。
不要试图在HOC中修改组件原型(或以其他方式改变它)。
function logProps(InputComponent){
InputComponent.prototype.componentDidUpdate=function(prevProps){
console.log('current props:',this.props)
console.log('previous props:',this.props)
}
return InputComponent
}
const EnhanceComponent = logProps(InputComponent)
这样会产生一些不良的后果。其一是输入组件再也无法像HOC增强之前那样使用了。更严重的是,如果你再用另一个永阳会修改componentDidUpdate
的HOC增强它,那么前面的HOC就会失效!同时,这个HOC也无法应用于没有声明周期的函数组件。
修改传入组件的HOC是一种糟糕的抽象方式。调用者必须知道他们是如何实现的,以避免与其他HOC发生冲突。
HOC不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能:
function logProps(InputComponent){
return class extends React.Component{
componentDidUpdate(prevProps){
console.log('current props:',this.props)
console.log('previous props:',this.props)
}
render(){
return <InputComponent {...this.props} />
}
}
}
该HOC与上文中修改传入组件的HOC功能相同,同时避免了出现冲突的情况。它同样适用于class组件和函数组件。而且因为它是一个纯函数,他可以与其他HOC组合,甚至可以与其自身组合。
您可能已经注意到HOC与容器组件模式之间有相似之处。容器组件担任分离将高层和底层关注的责任,由容器管理订阅和状态,并将prop传递给处理渲染UI。HOC使用容器座位其实现的一部分,你可以将HOC视为参数化容器组件。
约定:将不相关的props传递给被包裹的组件
HOC为组件添加特性。自身不应该大幅度改变约定。HOC返回的组件与原组件应保持类似的接口。
HOC应该透传与自身无关的props。大多数HOC都应该包含一个类似于下面的render方法。
render(){
const {extraProp,...passThroughProps} = this.props;
const inJectedProp = someStateOrInstanceMethod;
return (
<WrappedComponent
inJectedProp = { inJectedProp }
{...passThroughProps}
/>
)
}
这种约定保证了HOC的灵活性以及可复用性。
约定:最大化可组合性
并不是所有的HOC都一样。有时候它仅接受一个参数,也就是被包裹的组件:
const NavbarWithRouter = withRouter(Navbar)
HOC通常可以接受多个参数。比如在Relay中,HOC额外接收了一个配置对象用于指定组件的数据依赖:
const CommentWithRelay = Relay.createContainer(Comment,config)
最常见的HOC前面如下:
const ConnectedComment = connect(commentSelector,commentActions)(CommentList)
connect是一个返回高级组件的高级函数!
这种形式可能看起来令人困惑或不必要,但它有一个有用的属性。像connect函数返回的单参数HOC具有前面component => Component
。输出类型和输入类型相同的函数很容易组合在一起。
// 而不是这样
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// 你可以编写组合工具函数
// compose(f,g,h)等同于(...args) => f(g(h(...args)))
const enhance = compose(
withRouter,
connect(commentSelector)
)
const EnhanceComponent = enhance(WrappedComponent)
(同样的属性也允许connect
和其他HOC承担装饰器的角色,装饰器是一个实验性的JavaSscript提案。)
约定:包装显示名称以便轻松调试
HOC创建的容器组件会与任何其他组件一样,会显示在React Developer Tools
中。为了方便调试,请选择一个显示名称,以表明它是HOC的产物。
最常见的方式用HOC包装组件是显示名称。比如高级组件名为withSubscription
,并且被包装的组件显示名称为CommentList
,显示名称应该是withSubscription(CommentList)
:
function withSubscription(WrappedComponent){
class WithSuBscription extends React.Component{};
WithSuBscription.displayName = `WithSuBscription(${getDisplayName(WrappedComponent)})`
function getDisplayName(WrappedComponent){
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
}
注意事项
高级组件有一些需要注意的地方。
不要在render方法中使用HOC
React的diff算法(称为协调)使用组件表示来确定它是应该更新现有子树还是将其丢弃并挂载新子树。如果从render
返回的组件与前一个渲染中的组件相同(===
),则React通过将子树与新子树进行区分来递归更新子树。如果它们不相等,则完全卸载前一个子树。
通常,你不需要考虑这点。但对HOC来说这一点很重要,因为这代表着你不应在组件的render方法中对一个组件组件应用HOC:
render(){
// 每次调用render函数都穿件一个新的EnhancedComponent
const EnhanceComponent = enhance(MyComponent) // 每次返回的值都不相等,导致子树每次渲染都是进行卸载和重新挂载的操作。
return <EnhanceComponent />
}
这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。
如果在组件之外创建HOC,这样一来组件只会创建一次。因刺激每次render时候都会是同一个组件。一般来说,这跟你预期表现是一致的。
在极少数情况下。你需要动态调动HOC。你可以在组件的生命周期方法或其构造函数中进行调用。
务必复制静态方法
有时在React组件定义静态方法很有用。例如,Relay容器暴露了一个静态方法getFragment
以方便组合GraphQL片段。
但是,当你将HOC应用于组件时,原始组件将使用容器组件进行包装。这意味着新组建没有原始组件的任何静态方法。
// 定义静态函数
WrappedComponent.staticMethod = function(){}
// 现在使用HOC
const EnhanceComponent = enhance(WrappedComponent);
// 增强组件没有 staticeMethod
type EnhanceComponent.staticMethod === 'function' // true
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
function enhance(WrappedComponent){
class Enhance extends React.component {}
// 必须准确的知道应该拷贝那些方法
Enhance.staticeMethod = WrappedComponent.staticeMethod;
return Enhance
}
但是这样做,你需要知道那些方法应该拷贝。你可以使用hoist-non-react-statics
自动拷贝所有非React静态方法:
import hoistNonReactStatic from `hoist-non-react-statics`;
function enhance(WrappedComponent){
class Enhance extends React.Component{}
hoistNonReactStatic(Enhance,WrappedComponent);
return Enhance;
}
除了导出组件,另一种可行的方案是额外再导出一个静态方法。
MyComponent.someFunciton = someFunciton;
export default MyComponent;
export { someFunciton }
// 使用HOC解决横切关注点问题
inport MyComponent,{someFunciton} form './MyComponent.js'
Refs不会被传递
虽然高级组件的约定是将所有props传递给被包装组件,但是这对于refs并不适用。那是因为ref
实际上并不适一个prop-就想key一样,他是有React专门处理的。如果将ref添加带HOC的返回组件中,则ref引用只想容器组件,而不是被包装组件,