React文档(二十四)高阶组件
高阶组件(HOC)是React里的高级技术为了应对重用组件的逻辑。HOCs本质上不是React API的一部分。它是从React的组合性质中显露出来的模式。
具体来说,一个高阶组件就是一个获取一个组件并返回一个组件的函数。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
然而一个组件将props转变为UI,一个高阶组件将一个组件转变为另外一个组件。
HOCs在第三方React库里也是有的,就像Redux里的connect和Relay里的createContainer。
在这篇文档里,我们将讨论为什么高阶组件有用处,还有怎样来写你自己的高阶组件。
为横切关注点使用HOCs
注意:
我们以前建议使用mixins来处理横切关注点的问题。但现在意识到mixins会造成很多问题。读取更多信息关于为什么我们移除mixins以及怎样过渡已存在的组件。
在React里组件是主要的重用代码单元。然而,你会发现一些模式并不直接适合传统的组件。
举个例子,假设你有一个CommentList组件它订阅了一个外部数据源来渲染一组评论:
class CommentList extends React.Component {
constructor() {
super();
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>
);
}
}
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不完全相同。它们在DateSource上调用不同的方法,然后它们渲染出不同的输出。但是它们大多数实现是一样的:
- 在初始装载的时候,给DataSource添加一个监听改变的监听器
- 在监听器内部,当数据源变化的时候调用setState
- 销毁的时候,移除监听器
你可以想象在一个大型app里,订阅到DataSource并且调用setState这个同样的模式会一遍又一遍的重复发生。我们因此就想将这重复的过程抽象化,让我们在一个单独的地方定义这段逻辑并且在多个组件中都可以使用这段逻辑。这就是高阶组件所擅长的。
我们可以写一个创建组件的函数,就像CommentList和BlogPost,它们订阅到DataSource。这个函数会接受一个参数作为子组件,这个子组件接收订阅的数据作为prop。让我们调用这个函数eithSubscription:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
); const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
});
withSubscription的第一个参数是被包裹起来的组件。第二个参数检索我们喜欢的数据,给出一个DateSource和当前的props。
但CommentListWithSubscription和BlogPostWithSubscription被渲染了,CommenList和BlogPost将被传递一个data属性包含当前DataSource里检索出的数据。
// This function takes a component...
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} />;
}
};
}
注意一个HOC不会修改传入的参数组件,也不会使用继承来复制它的行为。不如说,一个HOC通过将原始组件包裹到一个容器组件里来混合。一个HOC是一个没有副作用的纯函数。
就是这样!被包裹的组件接受所有容器组件的props,还有和一个新的属性,data一起渲染它的输出。HOC不会关心数据怎样或者为什么使用,被包裹的组件也不会关心数据是从哪里来的。
因为withSubscription是一个普通的函数,你可以添加或多或少的参数根据情况。举个例子,你也许想要使data属性的名字是可配置的,这样就可以进一步从包裹的组件隔离HOC。或者你可以接受一个参数来配置shouldComponentUpdate,或者一个参数来配置数据源。这些都可以因为HOC拥有所有权利去定义组件。
类似于组件,withSubscription和被包裹的组件之间的不同是完全基于props的。这就可以很容易地去交换一个HOC和另一个,只要他们提供给被包裹组件的props是一样的。举个例子,这样如果你改变提取数据的库就会很有用。
不要改变原始组件,使用组合
在HOC里要打消修改组件原型的想法。
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps(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 received
const EnhancedComponent = logProps(InputComponent);
这里有几个问题。一个问题就是输入的组件不能通过增强后的组件来分离地重用。更重要的,如果你为EnhancedComponent运用其他HOC那样也还会改变componentWillReceiveProps,先前的HOC的功能会被重写!这个HOC也不能凭借函数式组件来工作,也没有生命周期方法。
改变HOC是一个脆弱的抽象,用户必须知道他们是怎样实现的为了避免和其他HOC发生冲突。
不用去修改,而应该使用组合,通过将输入的组件包裹到一个容器组件里:
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
这个HOC和修改后的HOC功能一样然而避免了潜在的冲突。它和类组件还有函数组件都工作地很好。因为它是纯函数,所以它可以由其他HOC组成,甚至用它自己也可以。
你也许注意到了HOC和容器组件这个模式的相似之处。容器组件是将高阶和低阶关注点的功能分离的策略的一部分。
容器管理类似订阅和state的东西,并且传递props给组件然后处理类似渲染UI的事。HOC将容器作为实现的一部分。你可以把HOC看做参数化的容器组件的定义。
约定:给被包裹的元素传递不相关的props
HOC给一个组件添加特性。它们不应该彻底改变它的约定。那就是HOC返回的组件拥有一个和被包裹的组件类似的界面。
HOC应该传递与确定的关注点不相关的props。多数HOC包含一个渲染方法看起来就像这样:
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}
/>
);
}
这个约定确保HOC尽可能地灵活和可重用。
约定:最大化可组合性
不是所有的高阶组件看起来都一样。有时候它们只接收一个参数,被包裹的组件:
const NavbarWithRouter = withRouter(Navbar);
通常HOC会接收额外的参数。在Relay的例子里,一个config对象被用于指定组件的数据依赖:
const CommentWithRelay = Relay.createContainer(Comment, config);
HOC最常见的签名是这样的:
// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
什么?!如果你将步骤分离,就可以很容易看出发生了什么。
// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is an HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);
换句话说,connect是一个返回高阶组件的高阶函数!
这种形式也许看起来让人迷惑或者不是很重要,但是它有一个有用的属性。单参数的HOC例如connect函数返回的那一个拥有这样的鲜明特征Component => Component(组件 => 组件)。输出类型和输入类型相同的函数就很容易组合到一起。
// Instead of doing this...
const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent)) // ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
// These are both single-argument HOCs
connect(commentSelector),
withRouter
)
const EnhancedComponent = enhance(WrappedComponent)
(这个一样的属性同样允许connect和其他增强器HOC被作为装饰来使用,这是一个实验性的js提案)
compose这个实用函数是很多第三方库提供的,包括lodash(lodash.flowRight),Redux,Ramda。
约定:包裹显示名字为了使调试更加简单
就像其他组件一样,HOC创建的容器组件也会显示在React Developer Tools工具里。想让调试更加简单,选择一个显示名字来通讯这是一个HOC的结果。
最普遍的技术是包裹被包裹函数的显示名字。所以如果你的高阶函数的名字是withSubscription,并且被包裹组件的显示名字是CommentList,那就使用显示名字WithSubscription(CommentList)。
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
} function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
说明
如果你是React新手,那么有一些有关高阶组件的说明不会立马就很明显。
不要在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 />;
}
这里的问题不只只是性能--一个组件的重载会造成组件的state和它所有的子元素丢失。
在组件定义的外部来使用HOC使得结果组件只被创建一次。之后,它的身份在渲染过程中会一直保持不变。总之,这就是你经常想要的结果。
在这些罕见的情况里你需要动态的运用HOC,你也可以在组建的生命周期函数里或者构造函数里使用。
静态方法必须被复制
有些时候在React组件里定义一个静态方法是很有用的。举个例子,Relay容器暴露了一个静态方法getFragment为了促进GraphQL片段的组成。
当你为一个组件运用了HOC,虽然原始组件被一个容器组件所包裹。这意味着新的组件没有任何原始组件的静态方法。
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply an HOC
const EnhancedComponent = enhance(WrappedComponent); // The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,你可以在返回它之前在容器组件之上复制那些方法。
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去自动复制所有非React的静态函数。
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
另一个可能的解决方案是分离地输出静态方法。
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent; // ...export the method separately...
export { someFunction }; // ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
React文档(二十四)高阶组件的更多相关文章
- React文档(十四)深入JSX
根本上,JSX只是为React.createElement(component, props, ...children)函数提供语法糖.JSX代码是这样的: <MyButton color=&q ...
- 微信小程序把玩(二十四)toast组件
原文:微信小程序把玩(二十四)toast组件 toast消息提示框,可用在提示一些信息,比如清楚缓存给用户一个友好的提示!或操作一些请求不想让用户有什么操作,toast也可以做到因为toast显示时其 ...
- React文档(十六)refs和DOM
Refs 提供了一种方式,用于访问在 render 方法中创建的 DOM 节点或 React 元素. 在标准的React数据流中,props是使得父组件和子组件之间交互的唯一方式.你通过props重新 ...
- React文档(十二)组合vs继承
React拥有很强大的组合模型,我们建议使用组合来替代继承来重利用组件之间的代码. 在本章节中,我们将讨论一些开发者经常触及继承的问题,并且我们该如何使用组合来解决这些问题. 组合 一些组件事先不知道 ...
- React文档(十九)不使用ES6
通常你会将一个React组件定义成一个普通的js类: class Greeting extends React.Component { render() { return <h1>Hell ...
- React文档(十八)最佳性能
在内部,React使用好几种聪明的技巧去最小化更新UI所需要的DOM操作.对于很多应用来说,使用React会使得构建用户界面非常之快而且不需要做太多专门的性能优化.虽然如此,还是有一些方法可以让你为R ...
- React文档(十五)使用propTypes进行类型检查
注意: React.PropTypes 自 React v15.5 起已弃用.请使用 prop-types 库代替. 随着你的应用的开发,你会使用类型检查的方法来捕获很多bug.对于一些应用,你可以使 ...
- React文档(十)表单
HTML表单元素和 React里的其他DOM元素有些不同,因为它们会保留一些内部的状态.举个例子,这个普通的表单接受唯一的name值: <form> <label> Name: ...
- React文档(十七)非受控组件
大多数情况下,我们建议使用受控组件(也就是用React的state来控制表单元素的value值)来实现表单.在一个受控组件里,表单数据被React组件处理.另一种方案就是非控制组件,这样的话表单数据就 ...
- react 项目实战(四)组件化表单/表单控件 高阶组件
高阶组件:formProvider 高阶组件就是返回组件的组件(函数) 为什么要通过一个组件去返回另一个组件? 使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能. 我们现在已经有 ...
随机推荐
- CentOS 7 zabbix添加监控服务器
CentOS 7 yum安装zabbix 设置中文界面 安装环境 CentOS 7 关闭防火墙和SElinux 在被监控端安装zabbix-agent [root@zabbix-agent ~]# ...
- LR参数化取值规则总结
我想使用参数化输入设置10个并发用户循环1000次,第一个用户使用参数列表中的前1000个参数(第依次循环使用第一个参数.第二次循环使用第二个参数,依次类推).第二个用户使用参数列表中的2001-30 ...
- Jmeter之Redis读写
Jmeter之Redis读写 奔跑的小小鱼 关注 0.2 2019.03.21 18:25* 字数 1330 阅读 45评论 0喜欢 1 Jmeter插件访问Redis共有3种方式: 1)通过自已 ...
- linux重启Oracle服务
linux重启oracle服务命令(完整版) (1) 以oracle身份登录数据库,命令:su – oracle (2) 进入Sqlplus控制台,命令:sqlplus /nolog (3) 以系统管 ...
- 类与对象 && 继承
以下是本人的对类与对象.继承的一些理解,如有错误之处万望谅解,如有朋友愿意指正,十分乐意,万分感谢! 类与对象 类与对象是学习编程的基础(大概吧),那么何为类?何为对象呢? 一.简 ...
- qemu中的内存管理
qemu负责模拟虚机的外设,因此虚机的线性地址空间主要由qemu进行管理,也就是确定线性地址空间中哪段地址属于哪个设备或者DRAM或者其他的什么. 1.数据结构 1.RAMBLOCK (最直接接触ho ...
- ABP入门系列之2——ABP模板项目
进入官网下载模板项目 依次按下图选择: 输入验证码开始下载 下载提示: 二.启动项目 使用VS2017打开项目,还原Nuget包: 设置以Web结尾的项目,设置为启动项目: 打开Web.config, ...
- k8s集群安装
准备三台虚拟机,一台做master,两台做master节点,关闭selinux. 一.安装docker,两node节点上进行 1. 2.安装docker依赖包:yum install -y yum-u ...
- k8s基本对象及架构
一.基本对象 pod pod是最小的部署单元,一个pod由一个或多个容器组成,pod中的容器共享存储和网络,在同一台docker主机上运行. service service是一个应用服务的抽象,定义了 ...
- git-bash的alias别名设置
正常需要设置别名时,直接使用 alias gs="git status" 输入上边的命令之后,就可以使用gs(命令)代替git status(命令),这是一种设置别名简化输入,提升 ...