React HOC 高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组件的函数。
实现方式
属性代理
返回用函数包裹原组件生成新组件,可以看做是被代理组件的父组件:
// proxyHOC.jsx
// hoc
const proxyHOC = (WrappedComponent) {
// 继承的是 React.Component
return class extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
// TestComponent.jsx
// 原组件
class TestComponent extends Component{
...
}
// 生成新组件
const TestWithHoc = proxyHOC(TestComponent);
// 将生成的新组件暴露出去
export default TestWithHoc
反向继承
返回一个继承原组件的新组件,因为是直接继承,所以可以访问到原组件的生命周期、props、state 以及 render 函数:
const inheritHOC = (WrappedComponent) {
// 继承的是原组件
return class extends WrappedComponent {
render() {
// super 关键字用于访问和调用一个对象的父对象上的函数
return super.render();
}
}
}
实现了什么功能
操作 props
在 HOC 内部可以对传递过来的 props 进行修改,再传给新组件:
const proxyHOC = (WrappedComponent) {
return class extends Component {
render() {
// 对 props 进行改造
const newProps = {
...this.props,
name:'Test'
}
return <WrappedComponent {...this.newProps} />;
}
}
}
获得 refs 的引用
在 HOC 内部,可以获取到新组件的 ref:
import React,{ Component, createRef } from 'react';
const proxyHOC = (WrappedComponent) => {
return class extends Component{
constructor(props) {
super(props);
this.ref = React.createRef();
}
render(){
return (
<WrappedComponent {...this.props} ref={this.ref} />
)
}
}
}
传递 ref 属性
ref 属性不属于 props,无法通过 props 向下传递(这里需要注意,不是 React.createRef 创造的值不能被传递,是 ref 这个属性无法被传递),举个例子:
// Parent.jsx
class Parent extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
render() {
return <Child ref={this.ref} data={'abc'}></Child>;
}
}
// Child.jsx
class Child extends Component {
constructor(props) {
super(props);
}
render() {
return <div>{ console.log(this.props) }</div>;
}
}
// output: { data:abc }
可以看到,并没有打印出 ref,可见 ref 并没有通过 props 被传递下去,因为 ref 不是 prop 属性,就像 key 一样,被 React 做了特殊的处理(不论是 function Component 还是 Class Component)。
而且,当 ref 指向高阶组件生成的新组件时,ref 其实指向的是高阶组件,举个例子:
// childplay.js
const ChildPlay = play(Child);
export default ChildPlay;
// another.js
<ChildPlay ref={this.ref}>
上面返回了 ChildPlay 组件,当我们对其 ref 赋值的时候,ref 实际上是指向了 play 而不是内部的 Child 组件,这意味着不能使用 ref.current.playMusic()
这样的方法。
解决这个问题,可以使用forwardRef
明确地将 ref 转发到内部的 Child 组件中去:
// Child.jsx
class Child extends Component {
constructor(props) {
super(props);
}
render() {
return <div className={"child"}>{console.log(this.props)}</div>;
}
}
const ChildPlay = play(Child); // 用hoc包裹Child组件
export default ChildPlay;
// playHoc.jsx
export const play = (WrappedComponent) => {
class InnerPlay extends Component {
render() {
const {forwardedRef,...props} = this.props;
return <WrappedComponent ref={forwardedRef} {...props} />;
}
}
return React.forwardRef((props,ref)=>{
// forwardRef方法获取的就是传递过来的ref属性
return <InnerPlay forwardedRef={ref} {...props}/>
})
};
上面高阶函数和最基础的高阶函数区别在于:
他不直接返回类组件,而是先创建一个内部的组件(InnerPlay);
然后调用React.forwardRef()
,并在该方法中,将父组件(Parent)传递过来的 ref 以 props 的形式传递给 InnerPlay;
再将 ref 通过 forwardedRef 赋值给 InnerPlay 组件的 props ;
最后 InnerPlay 组件将 forwardedRef 取出赋给目标组件的 ref 属性;
经过这样的操作,Parent 就能通过 ref 获取到 Child 组件了。
抽象state
即:将不受控组件转换为受控组件,举个例子:
class WrappedComponent extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
const proxyHOC = (WrappedComponent) => {
return 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} />;
}
}
}
用其他元素包装组件
在 render 中可以在组件外添加额外的元素,常用来增加布局和修改样式:
const proxyHOC = (WrappedComponent) =>{
return class extends Component{
render(){
return (
<div>
<WrapperComponent {...this.props}></WrapperComponent>
</div>
)
}
}
}
渲染劫持
举个例子:
function withAuth(WrappedComponent) {
return class extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
}
}
解决什么实际问题
逻辑复用
需要被复用的逻辑形成高阶函数,包裹目标组件,解决代码复用问题。例如常见的日志记录:
const logHoc = (WrappedComponent) {
return class extends Component {
componentDidCatch(error,info){
// 对错误做出响应
}
render() {
return <WrappedComponent {...this.props} />
}
}
}
权限控制
根据用户的权限,显示或隐藏具体内容
const auth = (WrappedComponent) {
return class extends Component {
render() {
// 判断用户是否具有权限
if( !hasPermission ) return null;
return <WrappedComponent {...props} />;
}
}
}
注意
HOC 不会也不应该去修改组件,它会生成一个新的组件,对原组件没有副作用;
不要在 render 方法中调用 HOC,因为每次调用 render 时都会生成一个新的组件,会在 diff 的时候被销毁:
render() { // 每次调用 render 函数都会创建一个新的 EnhancedComponent const EnhancedComponent = enhance(MyComponent); // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作! return <EnhancedComponent />; }
HOC 不会复制组件的静态方法,需要手动拷贝,举个例子:
// 定义静态函数 WrappedComponent.staticMethod = function() {/*...*/} // 现在使用 HOC const EnhancedComponent = enhance(WrappedComponent); // 增强组件没有 staticMethod typeof EnhancedComponent.staticMethod === 'undefined' // true
hoist-non-react-statics 库能够帮助我们批量拷贝静态方法。
为了在开发与调试时更好地区分 HOC,推荐设置 HOC 的显示名称:
const HOC = (WrappedComponent) =>{ return class extends Component{ static displayName = `HOC(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; render(){ return <WrapperComponent {...this.props} data={'ABC'}></WrapperComponent> } } }
传参
HOC 可以传递组件和其他参数,常用的是 HOC(…params)(WrappedComponent)
的形式,Redux 的 connect 、AntDesign 的 Form.Create 采用的都是这种形式:
// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(selector, actions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);
要使用此形式,需要将 HOC 稍微改写一下:
function HOC = (...params) => (WrappedCompoennt) =>{
...
}
React HOC 高阶组件