React 模式之高阶组件
高阶组件是一种用于代码逻辑复用的 React 模式,学习高阶组件,你可以:
- 写出更高效更合理的 React 代码。
- 理解第三方库中的高阶组件用法及其背后原理。
1. 什么是高阶组件
高阶组件 (Higher-Order Component, HOC) 是一种在 React 上实现代码复用的技术。它不是 React 的 API,而是一种编程模式。
具体点,HOC 是一个 Javascript 函数,它接收一个组件,并返回一个新的组件。
const EnhancedComponent = HOC(WrappedComponent);
HOC 对传入的组件进一步封装,添加特定的逻辑,形成新的组件并返回。
下面是一个最简单的 HOC,它不添加任何逻辑,返回一个与原组件相同的组件。
function hoc(WrappedComponent) {
const EnhancedComponent = (props) => {
// 这里添加你想要的逻辑
return <WrappedComponent {...props} />;
};
return EnhancedComponent;
}
“高阶组件”这个名称,衍生自函数式编程中的高阶函数(Higher-Order Function,HOF)。高阶函数指的是以函数为参数或者返回值为函数的函数,即可以处理函数的函数。JavaScript 中的 Array.map
、Array.reduce
都属于高阶函数。
HOC 与普通组件不同,没有返回一段用于渲染的 React 元素,而是把一个组件转换成另一个组件。从这点看,很难把它理解为“组件”,我倾向于理解成“组件转换函数”。
另一个理解的角度是,把函数调用视为“降阶”,高阶组件经过一次或多次降阶(调用),就可以得到一阶组件(普通组件)。
2. HOC 能做什么
HOC 是一个 JavaScript 函数,理论上能通过函数做到的,都能实现。在实际使用场景中,运用最多有:
- 引入副作用。
- 增加状态。
- props 代理。
- 控制渲染。
一个 HOC 可以同时做到以上的几点,和一些没有提及的功能。
2.1. 添加副作用
在 EnhancedComponent
中使用 useEffect
,可以在原组件的基础上增加副作用。
这个 HOC 返回的组件会在每次渲染时打印当前的 props。
function logProps(WrappedComponent) {
const EnhancedComponent = (props) => {
useEffect(() => {
console.log('本次渲染的 props:'+ props)
});
return <WrappedComponent {...props} />;
};
return EnhancedComponent;
2.2. 添加状态
类似地,使用 useState
可以为组件注入状态。
这个例子中,EnhancedComponent
可以监听 window.globalConfig
变量,当它变化时,在 1s 内刷新组件:
function SyncConfig(WrappedComponent) {
const EnhancedComponent = (props) => {
const [config, setConfig] = useState(window.globalConfig);
useEffect(() => {
const timer=setInterval(() => {
if(window.globalConfig!==config)
setConfig(window.globalConfig)
}, 1000);
return ()=>clearInterval(timer)
}, []);
return <WrappedComponent {...props} />;
};
return EnhancedComponent;
2.3. 代理 props
上面的例子中,都是用{...props}把组件 props 原封不动传递给原组件。不过,经常地,HOC 会修改一部分 props,比如:
- 注入一些额外的 props。
- 拦截特定的 props。
- 对 props 进行某些转换或,再传递给原组件。
上面的 SyncConfig
要求 WrappedComponent
依赖于 window.globalConfig
,这种设计并不合理,应该尽量让组件的依赖作为 props,我们来重写它,让 globalConfig
通过 props 传递给原组件。
function syncConfig(WrappedComponent) {
const EnhancedComponent = (props) => {
const [config, setConfig] = useState(window.globalConfig);
useEffect(() => {
const timer=setInterval(() => {
if(window.globalConfig!==config)
setConfig(window.globalConfig)
}, 1000);
return ()=>clearInterval(timer)
}, []);
return <WrappedComponent {...props} config={config} />;
};
return EnhancedComponent;
React Router v5 中的 withRouter 函数就是一个 HOC,它会给原组件传递 router 的 location
、match
、history
对象。
2.4. 控制渲染
EnhancedComponent
也可以在原组件的基础上,增加自己的渲染逻辑。
function withLoad(WrappedComponent) {
const EnhancedComponent = (props) => {
// useLoad 是一个封装了异步网络请求的hook
const { loading, data } = useLoad({
host:"https://example.com"
path: "/api/path",
method: "get",
});
if (loading) return <div>Loading ...</div>;
return <WrappedComponent {...props} data={data} />;
};
return EnhancedComponent;
}
上面的例子会在请求 loading 的时候返回一个加载态 UI。
2.5. 可配置的 HOC
HOC 本质上是 JavaScript 函数,可以利用函数实现更多的功能。
比如,可以给函数传递额外的参数,作为 HOC 的配置。上面的 withLoad
中,请求是固定的,其实很难被复用。可以把请求参数作为 withLoad
的参数,在创建的时候传入。
function withLoad(WrappedComponent, loadConfig) {
const EnhancedComponent = (props) => {
const { loading, data } = useLoad(loadConfig);
if (loading) return <div>Loading ...</div>;
return <WrappedComponent {...props} data={data} />;
};
return EnhancedComponent;
}
// 使用
const CardWithLoad = withLoad(Card, {
host:"https://example.com"
path: "/api/path",
method: "get",
});
完美,但又不完全完美。虽然每次请求的 path
和 method
都是不一样的,但在一个项目中 host
大概率是不变的。为了解决这个问题,我们可以向更高阶转换。
function createLoad(host) {
const withLoad = (WrappedComponent, loadConfig) => {
const EnhancedComponent = (props) => {
const { loading, data } = useLoad({ host, ...loadConfig });
if (loading) return <div>Loading ...</div>;
return <WrappedComponent {...props} data={data} />;
};
return EnhancedComponent;
};
return withLoad;
}
// 使用
const withLoad = createLoad("https://example.com");
const CardWithLoad = withLoad({ path: "/api/path", method: "get" });
3. 什么时候应该用 HOC
HOC 能够抽离出组件内的逻辑,并在需要的时候,把这个逻辑添加到组件中。而且,HOC 有很强的普适性,能应用到各类组件中。所以,HOC 适合用于抽离那些普遍存在于多个组件中的相同逻辑。
React 官方文档说 HOC 适合用于解决横切面关注点(Cross-Cutting Concern)问题。不过这个词有点抽象了,让人难以理解。不过,里面举的例子对 HOC 的使用场景很有启发,值得好好看看。
4. HOC 和其他模式的比较
4.1. HOC vs Hooks
HOC 的诞生早于 hooks。HOC 在 React 推出就存在了,hooks 在 16.8 才推出。
Hooks 也可以实现组件逻辑的抽离和复用,并且更容易编写和理解。 hooks 推出之后,在多场景下替代了 HOC,开发者更纷纷拥抱 hooks,而渐渐淡忘了 HOC。比如上面提到的 withRouter
,已经被 useLocation
等 hooks 取代,并于 React Router v6 版本废弃。
那我们还需要 HOC 吗?答案是肯定的,hooks 并不能完全取代 HOC,主要有以下两点:
- Hooks 不能完全覆盖组件生命周期函数的作用,HOC 可以。在 HOC 中,把 EnhancedComponent 定义成 class 组件,可以使用
shouldComponentUpdate
和componentDidCatch
等 hooks 不支持的生命周期函数。 - HOC 不会污染原组件。Hooks 直接在原组件内使用,HOC 则是在原组件的基础上创建一个其他版本的组件,原组件不受任何影响。
当然,在 hooks 可用的时候,应该优先使用 hooks。
4.2. HOC vs 组合
一个组件使用了另一个组件,这种模式就叫组合(composition)。组合是 React 最基础的模式,组件就是通过组合构建起 UI 的。
在 HOC 中,EnhancedComponent
使用了 WrappedComponent
, 所以 HOC 也是一种组合模式。一些 HOC 也能通过组合模式实现。
这个例子和上面的 syncConfig
效果一样:
function SyncConfig(props) {
const [config, setConfig] = useState(window.globalConfig);
useEffect(() => {
const timer = setInterval(() => {
if (window.globalConfig !== config) setConfig(window.globalConfig);
}, 1000);
return () => clearInterval(timer);
}, []);
return props.children;
}
// 使用
<SyncConfig>
<Child />
</SyncConfig>;
不过,在父组件中修改子组件非常不方便,远远不如 HOC 灵活。所以,这种组合方式适用于 UI 构建,在逻辑复用上很有限。
5. 一些需要注意的点
HOC 返回的是一个包裹着原组件的新组件,在使用时需要注意:
- 在最顶层作用域调用 HOC。不要在组件 render 函数内调用 HOC,因为每次返回的都是新组件,会导致组件状态复位和不必要的性能浪费。
- 手动进行 ref 转发。ref 不能通过
{...props}
转发,需要手动转发。 - 手动复制静态方法和属性。如果需要保留原组件的静态方法和属性,需要手动复制。
- Devtool 上能够看到包裹组件 EnhancedComponent 对应的组件节点。
6. 总结
HOC 是一个接收 React 组件并返回新组件的 JavaScript 函数,是一种实现逻辑复用的设计模式,广泛存在于 React 第三方库中。
使用 HOC 能够复用多个组件普遍存在的逻辑,现在大部分使用 hooks,不过 hooks 并不能完全取代它。
转载自:https://juejin.cn/post/7136834422933815332