一、基本概念
高阶函数是以函数为参数,并且返回值也是函数的函数。
高阶组件接收React组件作为参数,并且返回一个新的React组件。高阶组件本质上也是一个函数,并不是一个组件。
高阶组件的函数形式如下:
const EnhancedComponent = higherOrderComponent(WrappedComponent)
示例如下:
import Todo from "./components/Todo";
function App(props) {
const tasks = [
{ id: "todo-0", name: "Eat", completed: true },
{ id: "todo-1", name: "Sleep", completed: false },
{ id: "todo-2", name: "Repeat", completed: false }
];
const taskList = tasks.map(task => (<Todo name={task.name} key={task.id} />));
return (<ul>{taskList}</ul>);
}
export default App;
Todo.js:
//高阶组件
function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
this.setState({data: 11});
}
render () {
//this.props是WrappedComponent的props,data也将是WrappedComponent的props
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
//被包装的组件
class MyComponent extends Component {
render() {
return (
<div>
name:{this.props.name}<br/>
data:{this.props.data}
</div>
)
}
}
const Todo = withPersistentData(MyComponent)
export default Todo;
withPersistentData是一个高阶组件,返回一个新的组件,在新组件的componentWillMount中统一处理data,然后将获取到的数据通过props传递给被包装的组件WrappedComponent,这样WrappedComponent中可以直接使用this.props.data获取数据。
高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好的被复用。高阶组件的这种实现方式本质上是装饰者设计模式。
二、使用场景
2.1 操纵props
在被包装组件接收props前,高阶组件可以先拦截到props,对props执行增加、删除或修改的操作,然后将处理后的props再传递给被包装组件,上个例子就属于这种情况。
2.2 通过ref访问组件实例
下面的例子中,高阶组件保存WrappedComponent实例(MyComponent)在wrappedInstance,在someMethod中,调用MyComponent中的someMethodInWrappedComponent。
import Todo from "./components/Todo";
function App(props) {
let todoElm = null;
return (
<div className="todoapp stack-large">
<Todo ref={(el) => { todoElm = el; }} />
<button onClick={() => todoElm.someMethod()}>点击</button>
</div>
);
}
export default App;
function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
this.setState({ data: 11 });
}
someMethod() {
this.wrappedInstance.someMethodInWrappedComponent();
}
render() {
return <WrappedComponent ref={(instance) => {
this.wrappedInstance = instance
}} data={this.state.data} {...this.props} />
}
}
}
//被包装的组件
class MyComponent extends Component {
constructor(props) {
super(props);
this.someMethodInWrappedComponent = this.someMethodInWrappedComponent.bind(this);
}
someMethodInWrappedComponent(){
console.log(1);
}
render() {
return (<div>data:{this.props.data}</div>)
}
}
const Todo = withPersistentData(MyComponent)
export default Todo;
2.3 组件状态提升
高阶组件可以通过将被包装组件的状态及相应的状态处理方法提升到高阶组件自身内部,从而实现被包装组件的无状态化(无状态组件更容易被复用)。
下面的例子把受控组件value属性用到的状态和处理value变化的回调函数都提升到高阶组件中。SimpleControlledComponent变成无状态组件,状态由高阶组件维护。
function withPersistentData(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
}
this.handleValueChange = this.handleValueChange.bind(this);
}
handleValueChange(event) {
this.setState({
value: event.target.value
})
}
render() {
const newProps = {
controlledProps: {
value: this.state.value,
onChange: this.handleValueChange
}
}
return <WrappedComponent {...this.props} {...newProps} />
}
}
}
class SimpleControlledComponent extends Component {
render() {
return (
<input name="simple" {...this.props.controlledProps}/>
)
}
}
const Todo = withPersistentData(SimpleControlledComponent)
export default Todo;
2.4 用其他元素包装组件
常用于为WrappedComponent增加布局或修改样式。例如:
function withPersistentData(WrappedComponent) {
return class extends Component {
render() {
return (
<div style={{background: 'red'}}>
<WrappedComponent {...this.props} />
</div>
)
}
}
}
三、
高阶组件参数传递
高阶组件的参数并非只是一个组件,它还可以接收其他参数。
function withPersistentData(WrappedComponent,key) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({ data });
}
render() {
//通过{...this.props}把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
//被包装的组件
class MyComponent extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>data:{this.props.data}</div>
)
}
}
const TodoData = withPersistentData(MyComponent,'data')
const TodoName = withPersistentData(MyComponent,'name')
实际情况中,很少使用这种方式传递参数,而是采用更加灵活、更具通用性的函数形式:HOC(...params)(WrappedComponent)
HOC(...params)的返回值是一个高阶组件,高阶组件需要的参数是先传递给HOC函数的。改写如下:
function withPersistentData = (key) => (WrappedComponent) => {
//...
}
//被包装的组件
class MyComponent extends Component {
//...
}
const TodoData = withPersistentData('data')(MyComponent)
const TodoName = withPersistentData('name')(MyComponent)
这种形式的高阶组件大量出现在第三方库中,例如react-redux中的connect函数:
connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
connect的参数mapStateToProps、mapDispatchToProps是函数类型,说明高阶组件的参数也可以是函数类型。例如把组件ComponentA连接到Redux上的写法类似于:
connect(mapStateToProps,mapDispatchToProps)(ComponentA)
四、继承方式实现高阶组件
前面介绍的高阶组件的实现方式都是由高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,我们称这种实现方式为属性代理。
除了属性代理,还可以通过继承实现高阶组件:通过继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。
例如,当用户处于登录状态,允许组件渲染;否则渲染一个空组件:
function withPersistentData(WrappedComponent) {
return class extends WrappedComponent {
render() {
if(this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
}
}
继承方式实现的高阶组件对被包装组件具有侵入性,当组合多个高阶组件使用时,很容易因为子类组件忘记通过super调用父类组件方法而导致逻辑丢失。因此,在使用高阶组件时,应尽量通过代理方式实现高阶组件。
五、注意事项
- 为了在开发和调试阶段更好的区别包装了不同组件的高阶组件,需要对高阶组件的显示名称做自定义处理。常用的处理方法是,把被包装组件的显示名称也包到高阶组件的显示名称中。
- 不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为调用高阶组件,每次都会返回一个新组件,于是每次render,前一次高阶组件创建的组件都会被卸载,然后又重新挂载本次创建的新组件,既影响效率,又丢失组件及其子组件的状态。所以,高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响。
- 如果需要使用被包装组件的静态方法,那么必须手动复制这些静态方法。因为高阶组件返回的新组件不包含被包装组件的静态方法。
- refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref并不会传递给被包装组件。如果在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果希望获取被包装组件的引用,那么可以自定义一个属性,属性的值是一个函数,传递给被包装组件的ref。
- 与父组件的区别。高阶组件是一个函数,关注的是逻辑,父组件是一个组件,关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中;如果不直接相关,那么这部分逻辑适合使用高阶组件抽象,如数据请求。请求发送等。